From 2c1d70076605efbf009f1e590b6bcc4db2e8288f Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:37:49 +0200 Subject: [PATCH 01/29] Add `minify` option to plugin options --- packages/unplugin-typegpu/src/core/common.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index 827e5ce376..ea55baccda 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -26,6 +26,14 @@ export interface Options { /** @default true */ autoNamingEnabled?: boolean | undefined; + /** + * Minify the generated AST. + * This results in obfuscation of the generated WGSL, and in smaller bundle sizes. + * + * @default false + */ + minify?: boolean | undefined; + /** * Skipping files that don't contain "typegpu", "tgpu" or "use gpu". * In case this early pruning hinders transformation, you @@ -137,7 +145,8 @@ export const defaultOptions = { include: /\.m?[jt]sx?(?:\?.*)?$/, autoNamingEnabled: true, earlyPruning: true, -}; + minify: false, +} satisfies Partial; /** * Returns the block scope of a function declaration, if one exists. From e134114d430a5207c60c86f1f65c970c797a4714 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:57:07 +0200 Subject: [PATCH 02/29] Add minifier to parsers --- packages/tinyest-for-wgsl/src/minifier.ts | 27 +++++++++++++++++++++++ packages/tinyest-for-wgsl/src/parsers.ts | 9 ++++++-- packages/tinyest-for-wgsl/src/types.ts | 8 +++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 packages/tinyest-for-wgsl/src/minifier.ts diff --git a/packages/tinyest-for-wgsl/src/minifier.ts b/packages/tinyest-for-wgsl/src/minifier.ts new file mode 100644 index 0000000000..7da5df6d08 --- /dev/null +++ b/packages/tinyest-for-wgsl/src/minifier.ts @@ -0,0 +1,27 @@ +import type { Minifier } from './types.ts'; + +export class MinifierNullImpl implements Minifier { + minify(name: string): string { + return name; + } +} + +export class MinifierImpl implements Minifier { + #namesUsed = 0; + #nameMap: Map = new Map(); + + #generateFreshName() { + this.#namesUsed += 1; + return 'a'.repeat(this.#namesUsed); // TODO: implement this properly, take into account forbidden words like 'in' + } + + minify(name: string): string { + let minifiedName = this.#nameMap.get(name); + if (!minifiedName) { + minifiedName = this.#generateFreshName(); + this.#nameMap.set(name, minifiedName); + } + + return minifiedName; + } +} diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index 69299ac36d..f17fdb01f8 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -4,6 +4,7 @@ import * as tinyest from 'tinyest'; import { FuncParameterType } from 'tinyest'; import type { Context, JsNode, TranspilationResult } from './types.ts'; import { tryFindExternalChain } from './externals.ts'; +import { MinifierImpl, MinifierNullImpl } from './minifier.ts'; const { NodeTypeCatalog: NODE } = tinyest; @@ -408,13 +409,15 @@ export function extractFunctionParts(rootNode: JsNode): { }; } -export function transpileFn(rootNode: JsNode): TranspilationResult { +// TODO: make this parameter mandatory +export function transpileFn(rootNode: JsNode, minify = false): TranspilationResult { const { params, body } = extractFunctionParts(rootNode); const ctx: Context = { externalNames: new Set(), ignoreExternalDepth: 0, visitedNodes: new Set(), + minifier: minify ? new MinifierImpl() : new MinifierNullImpl(), stack: [ { declaredNames: params.flatMap((param) => @@ -443,11 +446,13 @@ export function transpileFn(rootNode: JsNode): TranspilationResult { }; } -export function transpileNode(node: JsNode): tinyest.AnyNode { +// TODO: make this parameter mandatory +export function transpileNode(node: JsNode, minify = false): tinyest.AnyNode { const ctx: Context = { externalNames: new Set(), ignoreExternalDepth: 0, visitedNodes: new Set(), + minifier: minify ? new MinifierImpl() : new MinifierNullImpl(), stack: [ { declaredNames: [], diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index f5a5f5ed4c..cce3724859 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -9,6 +9,10 @@ export type Scope = { export type Externals = Set; +export interface Minifier { + minify(name: string): string; +} + export type Context = { /** Holds a set of all identifiers that were used in code, but were not declared in code. */ externalNames: Externals; @@ -22,6 +26,10 @@ export type Context = { */ visitedNodes: Set; stack: Scope[]; + /** + * Used to transform identifiers. + */ + minifier: Minifier; }; export type TranspilationResult = { From 1b5162878d960117d1f4c4b0a0ae56e5c17200bd Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:08:28 +0200 Subject: [PATCH 03/29] Add some tests --- packages/tinyest-for-wgsl/tests/helpers.ts | 14 ++++ .../tests/minification.test.ts | 72 +++++++++++++++++++ .../tinyest-for-wgsl/tests/parsers.test.ts | 13 +--- 3 files changed, 87 insertions(+), 12 deletions(-) create mode 100644 packages/tinyest-for-wgsl/tests/helpers.ts create mode 100644 packages/tinyest-for-wgsl/tests/minification.test.ts diff --git a/packages/tinyest-for-wgsl/tests/helpers.ts b/packages/tinyest-for-wgsl/tests/helpers.ts new file mode 100644 index 0000000000..7bd671e13c --- /dev/null +++ b/packages/tinyest-for-wgsl/tests/helpers.ts @@ -0,0 +1,14 @@ +import babel from '@babel/parser'; +import type { Node } from '@babel/types'; +import * as acorn from 'acorn'; + +export const parseRollup = (code: string) => acorn.parse(code, { ecmaVersion: 'latest' }); +export const parseBabel = (code: string) => + babel.parse(code, { sourceType: 'module', plugins: ['typescript'] }).program.body[0] as Node; + +export function dualTest(test: (p: (code: string) => Node | acorn.AnyNode) => void) { + return () => { + test(parseBabel); + test(parseRollup); + }; +} diff --git a/packages/tinyest-for-wgsl/tests/minification.test.ts b/packages/tinyest-for-wgsl/tests/minification.test.ts new file mode 100644 index 0000000000..357abc014e --- /dev/null +++ b/packages/tinyest-for-wgsl/tests/minification.test.ts @@ -0,0 +1,72 @@ +import babel from '@babel/parser'; +import type { ClassDeclaration, ClassProperty, Expression, Node } from '@babel/types'; +import * as acorn from 'acorn'; +import { describe, expect, it } from 'vitest'; +import { transpileFn } from '../src/parsers.ts'; +import { dualTest } from './helpers.ts'; + +describe('transpileFn', () => { + it( + 'minifies used variables', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p('() => { const variable = 1; const other = 2; const sensitiveName = 3; }'), + true, + ); + + expect(params).toStrictEqual([]); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,"variable",[5,"1"]],[13,"other",[5,"2"]],[13,"sensitiveName",[5,"3"]]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(`Set {}`); + }), + ); + + it( + 'remembers minified names', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p('() => { const variable = 1; return variable; }'), + true, + ); + + expect(params).toStrictEqual([]); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,"variable",[5,"1"]],[13,"other",[5,"2"]],[13,"sensitiveName",[5,"3"]]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(`Set {}`); + }), + ); + + it( + 'minifies parameters', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p('(param1, param2) => { return param2 + param1; }'), + true, + ); + + expect(params).toStrictEqual([]); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,"variable",[5,"1"]],[13,"other",[5,"2"]],[13,"sensitiveName",[5,"3"]]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(`Set {}`); + }), + ); + + it( + 'does not minify struct params', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p('(param) => { const struct; return param.prop + struct.field; }'), + true, + ); + + expect(params).toStrictEqual([]); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,"variable",[5,"1"]],[13,"other",[5,"2"]],[13,"sensitiveName",[5,"3"]]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(`Set {}`); + }), + ); +}); diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index d46ceb01f7..3d076b0779 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -1,19 +1,8 @@ -import babel from '@babel/parser'; import type { ClassDeclaration, ClassProperty, Expression, Node } from '@babel/types'; import * as acorn from 'acorn'; import { describe, expect, it } from 'vitest'; import { transpileFn } from '../src/parsers.ts'; - -const parseRollup = (code: string) => acorn.parse(code, { ecmaVersion: 'latest' }); -const parseBabel = (code: string) => - babel.parse(code, { sourceType: 'module', plugins: ['typescript'] }).program.body[0] as Node; - -function dualTest(test: (p: (code: string) => Node | acorn.AnyNode) => void) { - return () => { - test(parseBabel); - test(parseRollup); - }; -} +import { dualTest, parseBabel } from './helpers.ts'; describe('transpileFn', () => { it( From 8ad03d1213b2cfc27e7da7d64bdf8b01551dcb77 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:53:13 +0200 Subject: [PATCH 04/29] Make basic examples work --- packages/tinyest-for-wgsl/src/externals.ts | 6 ++- packages/tinyest-for-wgsl/src/minifier.ts | 8 +++ packages/tinyest-for-wgsl/src/parsers.ts | 4 +- packages/tinyest-for-wgsl/src/types.ts | 8 +++ .../tests/minification.test.ts | 49 ++++++++++++++----- 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/externals.ts b/packages/tinyest-for-wgsl/src/externals.ts index 479ba914d4..633cc8bf2b 100644 --- a/packages/tinyest-for-wgsl/src/externals.ts +++ b/packages/tinyest-for-wgsl/src/externals.ts @@ -1,7 +1,11 @@ import type { Context, JsNode } from './types.ts'; function isDeclared(ctx: Context, name: string) { - return ctx.stack.some((scope) => scope.declaredNames.includes(name)); + const minifiedName = ctx.minifier.getIfMinified(name); + if (!minifiedName) { + return false; + } + return ctx.stack.some((scope) => scope.declaredNames.includes(minifiedName)); } /** diff --git a/packages/tinyest-for-wgsl/src/minifier.ts b/packages/tinyest-for-wgsl/src/minifier.ts index 7da5df6d08..c9c00394a5 100644 --- a/packages/tinyest-for-wgsl/src/minifier.ts +++ b/packages/tinyest-for-wgsl/src/minifier.ts @@ -4,6 +4,10 @@ export class MinifierNullImpl implements Minifier { minify(name: string): string { return name; } + getIfMinified(name: string) { + return name; + // TODO: reconsider, this may backfire + } } export class MinifierImpl implements Minifier { @@ -24,4 +28,8 @@ export class MinifierImpl implements Minifier { return minifiedName; } + + getIfMinified(name: string) { + return this.#nameMap.get(name); + } } diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index f17fdb01f8..b09873c852 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -53,7 +53,7 @@ const Transpilers: Partial<{ : [NODE.return], Identifier(ctx, node) { - return node.name; + return ctx.minifier.minify(node.name); }, ThisExpression() { @@ -180,7 +180,9 @@ const Transpilers: Partial<{ const decl = node.declarations[0]; ctx.ignoreExternalDepth++; + console.log(decl.id); const id = transpile(ctx, decl.id); + console.log(id); ctx.ignoreExternalDepth--; if (typeof id !== 'string') { diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index cce3724859..a8ec158bbc 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -10,7 +10,15 @@ export type Scope = { export type Externals = Set; export interface Minifier { + /** + * If `name` wasn't minified before, it gives it a new minified name. + * Then, returns the minified version of `name`. + */ minify(name: string): string; + /** + * Returns the minified version of `name` if it exists, otherwise returns undefined. + */ + getIfMinified(name: string): string | undefined; } export type Context = { diff --git a/packages/tinyest-for-wgsl/tests/minification.test.ts b/packages/tinyest-for-wgsl/tests/minification.test.ts index 357abc014e..d98a99d5b5 100644 --- a/packages/tinyest-for-wgsl/tests/minification.test.ts +++ b/packages/tinyest-for-wgsl/tests/minification.test.ts @@ -14,9 +14,9 @@ describe('transpileFn', () => { true, ); - expect(params).toStrictEqual([]); + expect(params).toMatchInlineSnapshot(`[]`); expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"variable",[5,"1"]],[13,"other",[5,"2"]],[13,"sensitiveName",[5,"3"]]]]"`, + `"[0,[[13,"a",[5,"1"]],[13,"aa",[5,"2"]],[13,"aaa",[5,"3"]]]]"`, ); expect(externalNames).toMatchInlineSnapshot(`Set {}`); }), @@ -30,9 +30,9 @@ describe('transpileFn', () => { true, ); - expect(params).toStrictEqual([]); + expect(params).toMatchInlineSnapshot(`[]`); expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"variable",[5,"1"]],[13,"other",[5,"2"]],[13,"sensitiveName",[5,"3"]]]]"`, + `"[0,[[13,"a",[5,"1"]],[10,"a"]]]"`, ); expect(externalNames).toMatchInlineSnapshot(`Set {}`); }), @@ -46,11 +46,27 @@ describe('transpileFn', () => { true, ); - expect(params).toStrictEqual([]); + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "param1", + "type": "i", + }, + { + "name": "param2", + "type": "i", + }, + ] + `); expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"variable",[5,"1"]],[13,"other",[5,"2"]],[13,"sensitiveName",[5,"3"]]]]"`, + `"[0,[[10,[1,"param2","+","param1"]]]]"`, ); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(` + Set { + "param2", + "param1", + } + `); }), ); @@ -58,15 +74,26 @@ describe('transpileFn', () => { 'does not minify struct params', dualTest((p) => { const { params, body, externalNames } = transpileFn( - p('(param) => { const struct; return param.prop + struct.field; }'), + p('(param) => { let struct; return param.prop + struct.field; }'), true, ); - expect(params).toStrictEqual([]); + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "param", + "type": "i", + }, + ] + `); expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"variable",[5,"1"]],[13,"other",[5,"2"]],[13,"sensitiveName",[5,"3"]]]]"`, + `"[0,[[12,"a"],[10,[1,"param.prop","+",[7,"a","aa"]]]]]"`, ); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(` + Set { + "param.prop", + } + `); }), ); }); From 7ac210dfa8234738f18587a3b2066306bebe1385 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:18:08 +0200 Subject: [PATCH 05/29] Support struct props --- packages/tinyest-for-wgsl/src/parsers.ts | 7 +++++++ packages/tinyest-for-wgsl/src/types.ts | 2 ++ packages/tinyest-for-wgsl/tests/minification.test.ts | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index b09873c852..1d72cba3fe 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -53,6 +53,9 @@ const Transpilers: Partial<{ : [NODE.return], Identifier(ctx, node) { + if (ctx.ignoreMinificationDepth > 0) { + return node.name; + } return ctx.minifier.minify(node.name); }, @@ -95,7 +98,9 @@ const Transpilers: Partial<{ // If the property is not computed, we don't want to register identifiers as external. ctx.ignoreExternalDepth++; + ctx.ignoreMinificationDepth++; const property = transpile(ctx, node.property) as tinyest.Expression; + ctx.ignoreMinificationDepth--; ctx.ignoreExternalDepth--; if (typeof property !== 'string') { @@ -418,6 +423,7 @@ export function transpileFn(rootNode: JsNode, minify = false): TranspilationResu const ctx: Context = { externalNames: new Set(), ignoreExternalDepth: 0, + ignoreMinificationDepth: 0, visitedNodes: new Set(), minifier: minify ? new MinifierImpl() : new MinifierNullImpl(), stack: [ @@ -453,6 +459,7 @@ export function transpileNode(node: JsNode, minify = false): tinyest.AnyNode { const ctx: Context = { externalNames: new Set(), ignoreExternalDepth: 0, + ignoreMinificationDepth: 0, visitedNodes: new Set(), minifier: minify ? new MinifierImpl() : new MinifierNullImpl(), stack: [ diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index a8ec158bbc..825a085d41 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -26,6 +26,8 @@ export type Context = { externalNames: Externals; /** Used to signal to identifiers that they should not treat their resolution as possible external uses. */ ignoreExternalDepth: number; + /** Used to signal to identifiers that they should not be minified (they are minified by default). */ + ignoreMinificationDepth: number; /** * Keeps the set of nodes visited by `tryFindExternalChain`. * This helps optimize code like `ext().x.y.z.t`: diff --git a/packages/tinyest-for-wgsl/tests/minification.test.ts b/packages/tinyest-for-wgsl/tests/minification.test.ts index d98a99d5b5..55e60092b2 100644 --- a/packages/tinyest-for-wgsl/tests/minification.test.ts +++ b/packages/tinyest-for-wgsl/tests/minification.test.ts @@ -87,7 +87,7 @@ describe('transpileFn', () => { ] `); expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[12,"a"],[10,[1,"param.prop","+",[7,"a","aa"]]]]]"`, + `"[0,[[12,"a"],[10,[1,"param.prop","+",[7,"a","field"]]]]]"`, ); expect(externalNames).toMatchInlineSnapshot(` Set { From dc8e0caf524a15c5b054435261747fcb154b756c Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:44:29 +0200 Subject: [PATCH 06/29] Support function params --- packages/tinyest-for-wgsl/src/parsers.ts | 66 ++++++++++--------- packages/tinyest-for-wgsl/src/types.ts | 1 + .../tests/minification.test.ts | 23 ++----- 3 files changed, 43 insertions(+), 47 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index 1d72cba3fe..7788a14ab8 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -2,7 +2,7 @@ import type * as babel from '@babel/types'; import type * as acorn from 'acorn'; import * as tinyest from 'tinyest'; import { FuncParameterType } from 'tinyest'; -import type { Context, JsNode, TranspilationResult } from './types.ts'; +import type { Context, JsNode, Minifier, Scope, TranspilationResult } from './types.ts'; import { tryFindExternalChain } from './externals.ts'; import { MinifierImpl, MinifierNullImpl } from './minifier.ts'; @@ -420,35 +420,20 @@ export function extractFunctionParts(rootNode: JsNode): { export function transpileFn(rootNode: JsNode, minify = false): TranspilationResult { const { params, body } = extractFunctionParts(rootNode); - const ctx: Context = { - externalNames: new Set(), - ignoreExternalDepth: 0, - ignoreMinificationDepth: 0, - visitedNodes: new Set(), - minifier: minify ? new MinifierImpl() : new MinifierNullImpl(), - stack: [ - { - declaredNames: params.flatMap((param) => - param.type === FuncParameterType.identifier - ? param.name - : param.props.map((prop) => prop.alias), - ), - }, - ], - }; + const ctx: Context = new ContextImpl(minify, params); const tinyestBody = transpile(ctx, body); if (body.type === 'BlockStatement') { return { - params, + params: ctx.params, body: tinyestBody as tinyest.Block, externalNames: ctx.externalNames, }; } return { - params, + params: ctx.params, body: [NODE.block, [[NODE.return, tinyestBody as tinyest.Expression]]], externalNames: ctx.externalNames, }; @@ -456,18 +441,37 @@ export function transpileFn(rootNode: JsNode, minify = false): TranspilationResu // TODO: make this parameter mandatory export function transpileNode(node: JsNode, minify = false): tinyest.AnyNode { - const ctx: Context = { - externalNames: new Set(), - ignoreExternalDepth: 0, - ignoreMinificationDepth: 0, - visitedNodes: new Set(), - minifier: minify ? new MinifierImpl() : new MinifierNullImpl(), - stack: [ - { - declaredNames: [], - }, - ], - }; + const ctx: Context = new ContextImpl(minify); return transpile(ctx, node); } + +class ContextImpl implements Context { + readonly externalNames: Set = new Set(); + ignoreExternalDepth = 0; + ignoreMinificationDepth = 0; + readonly visitedNodes: Set = new Set(); + readonly minifier: Minifier; + readonly stack: Scope[] = []; + readonly params: tinyest.FuncParameter[]; + + constructor(minify: boolean, params?: tinyest.FuncParameter[]) { + this.minifier = minify ? new MinifierImpl() : new MinifierNullImpl(); + this.params = (params ?? []).map((param) => { + if (param.type === FuncParameterType.identifier) { + return { ...param, name: this.minifier.minify(param.name) }; + } + return { + ...param, + props: param.props.map((prop) => ({ ...prop, alias: this.minifier.minify(prop.alias) })), + }; + }); + + const declaredNames = this.params.flatMap((param) => + param.type === FuncParameterType.identifier + ? param.name + : param.props.map((prop) => prop.alias), + ); + this.stack.push({ declaredNames }); + } +} diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index 825a085d41..e95ad06ed6 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -36,6 +36,7 @@ export type Context = { */ visitedNodes: Set; stack: Scope[]; + params: tinyest.FuncParameter[]; /** * Used to transform identifiers. */ diff --git a/packages/tinyest-for-wgsl/tests/minification.test.ts b/packages/tinyest-for-wgsl/tests/minification.test.ts index 55e60092b2..d617f64cf1 100644 --- a/packages/tinyest-for-wgsl/tests/minification.test.ts +++ b/packages/tinyest-for-wgsl/tests/minification.test.ts @@ -49,24 +49,19 @@ describe('transpileFn', () => { expect(params).toMatchInlineSnapshot(` [ { - "name": "param1", + "name": "a", "type": "i", }, { - "name": "param2", + "name": "aa", "type": "i", }, ] `); expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[10,[1,"param2","+","param1"]]]]"`, + `"[0,[[10,[1,"aa","+","a"]]]]"`, ); - expect(externalNames).toMatchInlineSnapshot(` - Set { - "param2", - "param1", - } - `); + expect(externalNames).toMatchInlineSnapshot(`Set {}`); }), ); @@ -81,19 +76,15 @@ describe('transpileFn', () => { expect(params).toMatchInlineSnapshot(` [ { - "name": "param", + "name": "a", "type": "i", }, ] `); expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[12,"a"],[10,[1,"param.prop","+",[7,"a","field"]]]]]"`, + `"[0,[[12,"aa"],[10,[1,[7,"a","prop"],"+",[7,"aa","field"]]]]]"`, ); - expect(externalNames).toMatchInlineSnapshot(` - Set { - "param.prop", - } - `); + expect(externalNames).toMatchInlineSnapshot(`Set {}`); }), ); }); From 4ab2e8012313fa34c64f79e82e881efd5baeab0a Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:12:11 +0200 Subject: [PATCH 07/29] Add more parameter tests --- .../tests/minification.test.ts | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/packages/tinyest-for-wgsl/tests/minification.test.ts b/packages/tinyest-for-wgsl/tests/minification.test.ts index d617f64cf1..a96094cddd 100644 --- a/packages/tinyest-for-wgsl/tests/minification.test.ts +++ b/packages/tinyest-for-wgsl/tests/minification.test.ts @@ -31,9 +31,7 @@ describe('transpileFn', () => { ); expect(params).toMatchInlineSnapshot(`[]`); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"a",[5,"1"]],[10,"a"]]]"`, - ); + expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[13,"a",[5,"1"]],[10,"a"]]]"`); expect(externalNames).toMatchInlineSnapshot(`Set {}`); }), ); @@ -58,9 +56,71 @@ describe('transpileFn', () => { }, ] `); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[10,[1,"aa","+","a"]]]]"`, + expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"aa","+","a"]]]]"`); + expect(externalNames).toMatchInlineSnapshot(`Set {}`); + }), + ); + + it( + 'minifies destructured parameters', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p('(param, { prop }) => { return param + prop; }'), + true, + ); + + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "a", + "type": "i", + }, + { + "props": [ + { + "alias": "aa", + "name": "prop", + }, + ], + "type": "d", + }, + ] + `); + expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"a","+","aa"]]]]"`); + expect(externalNames).toMatchInlineSnapshot(`Set {}`); + }), + ); + + it( + 'minifies destructured parameters with aliases', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p('(param, { prop, other: alias }) => { return param + prop + alias; }'), + true, ); + + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "a", + "type": "i", + }, + { + "props": [ + { + "alias": "aa", + "name": "prop", + }, + { + "alias": "aaa", + "name": "other", + }, + ], + "type": "d", + }, + ] + `); + expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,[1,"a","+","aa"],"+","aaa"]]]]"`); expect(externalNames).toMatchInlineSnapshot(`Set {}`); }), ); From dd2fc0d6d85cd455b660e867259e613763ef4c13 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:06:01 +0200 Subject: [PATCH 08/29] Generate minified identifiers properly --- packages/tinyest-for-wgsl/src/minifier.ts | 83 ++++++++++++++++++- .../tests/minification.test.ts | 56 +++++++++++-- 2 files changed, 126 insertions(+), 13 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/minifier.ts b/packages/tinyest-for-wgsl/src/minifier.ts index c9c00394a5..36eda2990c 100644 --- a/packages/tinyest-for-wgsl/src/minifier.ts +++ b/packages/tinyest-for-wgsl/src/minifier.ts @@ -10,13 +10,42 @@ export class MinifierNullImpl implements Minifier { } } +/** + * Generates all strings consisting of lowercase letters of the given length. + */ +function* combinationGenerator(length: number): Generator { + if (length === 0) { + yield ''; + return; + } + + for (let i = 97 /* ASCII a */; i <= 122 /* ASCII z */; i++) { + for (const name of combinationGenerator(length - 1)) { + yield `${String.fromCharCode(i)}${name}`; + } + } +} + +/** + * Generates fresh minified names, avoids forbidden tokens. + */ +function* freshNameGenerator(): Generator { + for (let i = 1; i <= 4; i++) { + for (const name of combinationGenerator(i)) { + if (!bannedTokens.has(name)) { + yield name; + } + } + } + throw new Error('Too many variable names!'); +} + export class MinifierImpl implements Minifier { - #namesUsed = 0; #nameMap: Map = new Map(); + #nameGenerator: Generator = freshNameGenerator(); - #generateFreshName() { - this.#namesUsed += 1; - return 'a'.repeat(this.#namesUsed); // TODO: implement this properly, take into account forbidden words like 'in' + #generateFreshName(): string { + return this.#nameGenerator.next().value; } minify(name: string): string { @@ -33,3 +62,49 @@ export class MinifierImpl implements Minifier { return this.#nameMap.get(name); } } + +export const bannedTokens = new Set([ + 'case', + 'else', + 'fn', + 'for', + 'if', + 'let', + 'loop', + 'true', + 'var', + 'NULL', + 'Self', + 'as', + 'asm', + 'auto', + 'cast', + 'do', + 'enum', + 'from', + 'get', + 'goto', + 'impl', + 'lowp', + 'meta', + 'mod', + 'move', + 'mut', + 'new', + 'nil', + 'null', + 'of', + 'pass', + 'priv', + 'pub', + 'ref', + 'self', + 'set', + 'std', + 'this', + 'try', + 'type', + 'use', + 'wgsl', + 'with', +]); diff --git a/packages/tinyest-for-wgsl/tests/minification.test.ts b/packages/tinyest-for-wgsl/tests/minification.test.ts index a96094cddd..3ba49c0581 100644 --- a/packages/tinyest-for-wgsl/tests/minification.test.ts +++ b/packages/tinyest-for-wgsl/tests/minification.test.ts @@ -16,7 +16,7 @@ describe('transpileFn', () => { expect(params).toMatchInlineSnapshot(`[]`); expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"a",[5,"1"]],[13,"aa",[5,"2"]],[13,"aaa",[5,"3"]]]]"`, + `"[0,[[13,"a",[5,"1"]],[13,"b",[5,"2"]],[13,"c",[5,"3"]]]]"`, ); expect(externalNames).toMatchInlineSnapshot(`Set {}`); }), @@ -51,12 +51,12 @@ describe('transpileFn', () => { "type": "i", }, { - "name": "aa", + "name": "b", "type": "i", }, ] `); - expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"aa","+","a"]]]]"`); + expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"b","+","a"]]]]"`); expect(externalNames).toMatchInlineSnapshot(`Set {}`); }), ); @@ -78,7 +78,7 @@ describe('transpileFn', () => { { "props": [ { - "alias": "aa", + "alias": "b", "name": "prop", }, ], @@ -86,7 +86,7 @@ describe('transpileFn', () => { }, ] `); - expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"a","+","aa"]]]]"`); + expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"a","+","b"]]]]"`); expect(externalNames).toMatchInlineSnapshot(`Set {}`); }), ); @@ -108,11 +108,11 @@ describe('transpileFn', () => { { "props": [ { - "alias": "aa", + "alias": "b", "name": "prop", }, { - "alias": "aaa", + "alias": "c", "name": "other", }, ], @@ -120,7 +120,9 @@ describe('transpileFn', () => { }, ] `); - expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,[1,"a","+","aa"],"+","aaa"]]]]"`); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[10,[1,[1,"a","+","b"],"+","c"]]]]"`, + ); expect(externalNames).toMatchInlineSnapshot(`Set {}`); }), ); @@ -142,9 +144,45 @@ describe('transpileFn', () => { ] `); expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[12,"aa"],[10,[1,[7,"a","prop"],"+",[7,"aa","field"]]]]]"`, + `"[0,[[12,"b"],[10,[1,[7,"a","prop"],"+",[7,"b","field"]]]]]"`, ); expect(externalNames).toMatchInlineSnapshot(`Set {}`); }), ); + + // TODO: externals + // TODO: shadowing + + it( + 'supports more than 26 names', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p(`() => { ${Array.from({ length: 100 }, (_, i) => `let v${i};`).join('\n')} }`), + true, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + const stringifiedBody = JSON.stringify(body); + expect(stringifiedBody).toContain('z'); + expect(stringifiedBody).toContain('aa'); + expect(stringifiedBody).toContain('ab'); + expect(externalNames).toMatchInlineSnapshot(`Set {}`); + }), + ); + + it( + 'omits reserved words', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p(`() => { ${Array.from({ length: 26 + 26 * 26 }, (_, i) => `let v${i};`).join('\n')} }`), + true, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + const stringifiedBody = JSON.stringify(body); + expect(stringifiedBody).not.toContain('if'); + expect(stringifiedBody).toContain('aaa'); + expect(externalNames).toMatchInlineSnapshot(`Set {}`); + }), + ); }); From 612514716770ea37177e27fe2e1db6944be55b03 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:22:39 +0200 Subject: [PATCH 09/29] CHange externals set to map, handle externals --- packages/tinyest-for-wgsl/src/parsers.ts | 7 +- packages/tinyest-for-wgsl/src/types.ts | 2 +- .../tests/minification.test.ts | 62 +++++++++++++--- .../tinyest-for-wgsl/tests/parsers.test.ts | 74 +++++++++---------- packages/unplugin-typegpu/src/babel.ts | 4 +- packages/unplugin-typegpu/src/core/factory.ts | 2 +- 6 files changed, 98 insertions(+), 53 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index 7788a14ab8..39e6294d1c 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -309,8 +309,9 @@ function transpile(ctx: Context, node: JsNode): tinyest.AnyNode { // add it to externals and swap the AST node for an identifier. const externalChain = tryFindExternalChain(ctx, node); if (externalChain) { - ctx.externalNames.add(externalChain); - return externalChain; + const minified = ctx.minifier.minify(externalChain); + ctx.externalNames.set(minified, externalChain); + return minified; } } @@ -447,7 +448,7 @@ export function transpileNode(node: JsNode, minify = false): tinyest.AnyNode { } class ContextImpl implements Context { - readonly externalNames: Set = new Set(); + readonly externalNames: Map = new Map(); ignoreExternalDepth = 0; ignoreMinificationDepth = 0; readonly visitedNodes: Set = new Set(); diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index e95ad06ed6..08e4bfa794 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -7,7 +7,7 @@ export type Scope = { declaredNames: string[]; }; -export type Externals = Set; +export type Externals = Map; export interface Minifier { /** diff --git a/packages/tinyest-for-wgsl/tests/minification.test.ts b/packages/tinyest-for-wgsl/tests/minification.test.ts index 3ba49c0581..f75e430f90 100644 --- a/packages/tinyest-for-wgsl/tests/minification.test.ts +++ b/packages/tinyest-for-wgsl/tests/minification.test.ts @@ -18,7 +18,7 @@ describe('transpileFn', () => { expect(JSON.stringify(body)).toMatchInlineSnapshot( `"[0,[[13,"a",[5,"1"]],[13,"b",[5,"2"]],[13,"c",[5,"3"]]]]"`, ); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); }), ); @@ -32,7 +32,7 @@ describe('transpileFn', () => { expect(params).toMatchInlineSnapshot(`[]`); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[13,"a",[5,"1"]],[10,"a"]]]"`); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); }), ); @@ -57,7 +57,7 @@ describe('transpileFn', () => { ] `); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"b","+","a"]]]]"`); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); }), ); @@ -87,7 +87,7 @@ describe('transpileFn', () => { ] `); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"a","+","b"]]]]"`); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); }), ); @@ -123,7 +123,7 @@ describe('transpileFn', () => { expect(JSON.stringify(body)).toMatchInlineSnapshot( `"[0,[[10,[1,[1,"a","+","b"],"+","c"]]]]"`, ); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); }), ); @@ -146,11 +146,55 @@ describe('transpileFn', () => { expect(JSON.stringify(body)).toMatchInlineSnapshot( `"[0,[[12,"b"],[10,[1,[7,"a","prop"],"+",[7,"b","field"]]]]]"`, ); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); + + it( + "minifies 'this'", + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p(`() => { return this.prop1.prop2; }`), + true, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,"a"]]]"`); + expect(externalNames).toMatchInlineSnapshot(` + Map { + "a" => "this.prop1.prop2", + } + `); + }), + ); + + it( + 'minifies externals', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p(`() => { + const var1 = ext.value; + const var2 = ext.config.multiplier; + const var3 = ext.config.zero; + const var4 = ext.config.multiplier; + }`), + true, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,"a","b"],[13,"c","d"],[13,"e","f"],[13,"g","d"]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(` + Map { + "b" => "ext.value", + "d" => "ext.config.multiplier", + "f" => "ext.config.zero", + } + `); }), ); - // TODO: externals // TODO: shadowing it( @@ -166,7 +210,7 @@ describe('transpileFn', () => { expect(stringifiedBody).toContain('z'); expect(stringifiedBody).toContain('aa'); expect(stringifiedBody).toContain('ab'); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); }), ); @@ -182,7 +226,7 @@ describe('transpileFn', () => { const stringifiedBody = JSON.stringify(body); expect(stringifiedBody).not.toContain('if'); expect(stringifiedBody).toContain('aaa'); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); }), ); }); diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index 3d076b0779..7204375792 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -19,7 +19,7 @@ describe('transpileFn', () => { expect(params).toStrictEqual([]); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[]]"`); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); }), ); @@ -30,7 +30,7 @@ describe('transpileFn', () => { expect(params).toStrictEqual([]); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[]]"`); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); }), ); @@ -47,8 +47,8 @@ describe('transpileFn', () => { `"[0,[[10,[1,[1,"a","+","b"],"-","c"]]]]"`, ); expect(externalNames).toMatchInlineSnapshot(` - Set { - "c", + Map { + "c" => "c", } `); }), @@ -70,8 +70,8 @@ describe('transpileFn', () => { ); // Only 'c' is external, as 'a' is declared in the same scope. expect(externalNames).toMatchInlineSnapshot(` - Set { - "c", + Map { + "c" => "c", } `); }), @@ -95,8 +95,8 @@ describe('transpileFn', () => { ); // Only 'c' is external, as 'a' is declared in the outer scope. expect(externalNames).toMatchInlineSnapshot(` - Set { - "c", + Map { + "c" => "c", } `); }), @@ -111,8 +111,8 @@ describe('transpileFn', () => { expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,"external.outside.prop"]]]"`); // Only 'external' is external. expect(externalNames).toMatchInlineSnapshot(` - Set { - "external.outside.prop", + Map { + "external.outside.prop" => "external.outside.prop", } `); }), @@ -143,7 +143,7 @@ describe('transpileFn', () => { }, ]); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); }), ); @@ -189,7 +189,7 @@ describe('transpileFn', () => { }, ]); - expect(externalNames).toMatchInlineSnapshot(`Set {}`); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); }), ); @@ -214,8 +214,8 @@ describe('transpileFn', () => { ); expect(externalNames).toMatchInlineSnapshot(` - Set { - "a", + Map { + "a" => "a", } `); }), @@ -236,8 +236,8 @@ describe('transpileFn', () => { ); expect(externalNames).toMatchInlineSnapshot(` - Set { - "a", + Map { + "a" => "a", } `); }), @@ -270,19 +270,19 @@ describe('transpileFn', () => { ); expect(externalNames).toMatchInlineSnapshot(` - Set { - "ext.p", - "ext.q.a", - "ext.q.b", - "ext.r.a", - "ext.r", - "ext.s", - "ext.s.a", - "ext.t.fn", - "ext.t.comp", - "ext.t", - "ext.u", - "ext", + Map { + "ext.p" => "ext.p", + "ext.q.a" => "ext.q.a", + "ext.q.b" => "ext.q.b", + "ext.r.a" => "ext.r.a", + "ext.r" => "ext.r", + "ext.s" => "ext.s", + "ext.s.a" => "ext.s.a", + "ext.t.fn" => "ext.t.fn", + "ext.t.comp" => "ext.t.comp", + "ext.t" => "ext.t", + "ext.u" => "ext.u", + "ext" => "ext", } `); @@ -303,8 +303,8 @@ describe('transpileFn', () => { ); expect(externalNames).toMatchInlineSnapshot(` - Set { - "ext", + Map { + "ext" => "ext", } `); }), @@ -323,10 +323,10 @@ describe('transpileFn', () => { ); expect(externalNames).toMatchInlineSnapshot(` - Set { - "ext.value", - "ext.config.multiplier", - "ext.config.zero", + Map { + "ext.value" => "ext.value", + "ext.config.multiplier" => "ext.config.multiplier", + "ext.config.zero" => "ext.config.zero", } `); @@ -358,8 +358,8 @@ describe('transpileFn', () => { const { externalNames } = transpileFn(fn); expect(externalNames).toMatchInlineSnapshot(` - Set { - "this.#v", + Map { + "this.#v" => "this.#v", } `); }), diff --git a/packages/unplugin-typegpu/src/babel.ts b/packages/unplugin-typegpu/src/babel.ts index a4495214c1..ceb66a99d0 100644 --- a/packages/unplugin-typegpu/src/babel.ts +++ b/packages/unplugin-typegpu/src/babel.ts @@ -19,8 +19,8 @@ function i(identifier: string): t.Identifier { function externalsToNode(externals: Externals): t.Expression { return t.objectExpression( - Array.from(externals, (key) => { - const chain = key.split('.'); + Array.from(externals, ([key, value]) => { + const chain = value.split('.'); if (!chain[0]) { throw new Error('Internal error, expected chain to not be empty'); } diff --git a/packages/unplugin-typegpu/src/core/factory.ts b/packages/unplugin-typegpu/src/core/factory.ts index 4668894469..c18b76397f 100644 --- a/packages/unplugin-typegpu/src/core/factory.ts +++ b/packages/unplugin-typegpu/src/core/factory.ts @@ -33,7 +33,7 @@ function embedJSON(jsValue: unknown) { } function externalsToString(externals: Externals): string { - const entries = Array.from(externals, (key) => `"${key}":() => ${key}`); + const entries = Array.from(externals, ([key, value]) => `"${key}":() => ${value}`); return `{${entries.join(',')}}`; } From cde0469232fbd89cddaab686f645c74349197882 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:31:02 +0200 Subject: [PATCH 10/29] Add more tests --- .../tests/minification.test.ts | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/packages/tinyest-for-wgsl/tests/minification.test.ts b/packages/tinyest-for-wgsl/tests/minification.test.ts index f75e430f90..84d0517a96 100644 --- a/packages/tinyest-for-wgsl/tests/minification.test.ts +++ b/packages/tinyest-for-wgsl/tests/minification.test.ts @@ -195,7 +195,89 @@ describe('transpileFn', () => { }), ); - // TODO: shadowing + it( + 'minifies complex externals', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p(`() => { + const h = ext.t.fn().prop; + const i = ext.t.comp['computed'].prop; + const j = ext.t.$.prop; + const k = (ext).prop; + }`), + true, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,"a",[7,[6,"b",[]],"prop"]],[13,"c",[7,[8,"d",[103,"computed"]],"prop"]],[13,"e",[7,[7,"f","$"],"prop"]],[13,"g","h"]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(` + Map { + "b" => "ext.t.fn", + "d" => "ext.t.comp", + "f" => "ext.t", + "h" => "ext.prop", + } + `); + }), + ); + + it( + 'correctly handles variable shadowing', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p(`() => { + const variable = 1; + { + const variable = 2; + if (false) { + return variable; + } + } + return variable; + }`), + true, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,"a",[5,"1"]],[0,[[13,"a",[5,"2"]],[11,false,[0,[[10,"a"]]]]]],[10,"a"]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); + + it( + 'correctly handles parameter shadowing', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p(`(parameter) => { + { + const parameter = 2; + if (false) { + return parameter; + } + } + return parameter; + }`), + true, + ); + + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "a", + "type": "i", + }, + ] + `); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[0,[[13,"a",[5,"2"]],[11,false,[0,[[10,"a"]]]]]],[10,"a"]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); it( 'supports more than 26 names', From 626992f4db2306ffd5e79ef43c32cf0a0a9a7173 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:58:23 +0200 Subject: [PATCH 11/29] Fix object expression --- packages/tinyest-for-wgsl/src/parsers.ts | 2 + .../tests/minification.test.ts | 52 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index 39e6294d1c..8f8fd2a5e1 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -239,10 +239,12 @@ const Transpilers: Partial<{ } ctx.ignoreExternalDepth++; + ctx.ignoreMinificationDepth++; const key = prop.key.type === 'Identifier' ? (transpile(ctx, prop.key) as string) : String(prop.key.value); + ctx.ignoreMinificationDepth--; ctx.ignoreExternalDepth--; const value = transpile(ctx, prop.value) as tinyest.Expression; diff --git a/packages/tinyest-for-wgsl/tests/minification.test.ts b/packages/tinyest-for-wgsl/tests/minification.test.ts index 84d0517a96..cfb799ae48 100644 --- a/packages/tinyest-for-wgsl/tests/minification.test.ts +++ b/packages/tinyest-for-wgsl/tests/minification.test.ts @@ -128,7 +128,7 @@ describe('transpileFn', () => { ); it( - 'does not minify struct params', + 'does not minify struct props', dualTest((p) => { const { params, body, externalNames } = transpileFn( p('(param) => { let struct; return param.prop + struct.field; }'), @@ -150,6 +150,29 @@ describe('transpileFn', () => { }), ); + it( + 'does not minify struct keys', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p('(param) => { let struct = { field: 1 }; return struct.field; }'), + true, + ); + + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "a", + "type": "i", + }, + ] + `); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[12,"b",[104,{"field":[5,"1"]}]],[10,[7,"b","field"]]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); + it( "minifies 'this'", dualTest((p) => { @@ -279,6 +302,33 @@ describe('transpileFn', () => { }), ); + it( + 'correctly handles external shadowing', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p(`() => { + const variable = external; + { + const external = 1; + return external; + } + return external; + }`), + true, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,"a","b"],[0,[[13,"b",[5,"1"]],[10,"b"]]],[10,"b"]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(` + Map { + "b" => "external", + } + `); + }), + ); + it( 'supports more than 26 names', dualTest((p) => { From 29b411652acaeb565f96d04fa79310e370b75df7 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:02:13 +0200 Subject: [PATCH 12/29] Add more tests --- packages/tinyest-for-wgsl/src/parsers.ts | 2 -- .../tests/minification.test.ts | 32 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index 8f8fd2a5e1..5e06feeb7d 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -185,9 +185,7 @@ const Transpilers: Partial<{ const decl = node.declarations[0]; ctx.ignoreExternalDepth++; - console.log(decl.id); const id = transpile(ctx, decl.id); - console.log(id); ctx.ignoreExternalDepth--; if (typeof id !== 'string') { diff --git a/packages/tinyest-for-wgsl/tests/minification.test.ts b/packages/tinyest-for-wgsl/tests/minification.test.ts index cfb799ae48..3aaf9de53a 100644 --- a/packages/tinyest-for-wgsl/tests/minification.test.ts +++ b/packages/tinyest-for-wgsl/tests/minification.test.ts @@ -36,6 +36,38 @@ describe('transpileFn', () => { }), ); + it( + 'remembers minified names in computed access', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p('() => { const variable = 1; const array = [1, 2]; return array[variable]; }'), + true, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,"a",[5,"1"]],[13,"b",[100,[[5,"1"],[5,"2"]]]],[10,[8,"b","a"]]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); + + it( + 'remembers minified names in for loops', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p('() => { for (let i = 0; i< 10; i++) { return i; } }'), + true, + ); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[14,[12,"a",[5,"0"]],[1,"a","<",[5,"10"]],[102,"++","a"],[0,[[10,"a"]]]]]]"`, + ); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }), + ); + it( 'minifies parameters', dualTest((p) => { From ab8e565f67d17cbda1dee869eefb21a524e1ce8e Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:46:27 +0200 Subject: [PATCH 13/29] Allow plugins to minify --- packages/tinyest-for-wgsl/src/parsers.ts | 3 +- .../tinyest-for-wgsl/tests/parsers.test.ts | 23 ++++--- packages/unplugin-typegpu/src/core/common.ts | 16 ++--- .../test/minification.test.ts | 63 +++++++++++++++++++ 4 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 packages/unplugin-typegpu/test/minification.test.ts diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index 5e06feeb7d..ec0ae6e748 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -417,8 +417,7 @@ export function extractFunctionParts(rootNode: JsNode): { }; } -// TODO: make this parameter mandatory -export function transpileFn(rootNode: JsNode, minify = false): TranspilationResult { +export function transpileFn(rootNode: JsNode, minify: boolean): TranspilationResult { const { params, body } = extractFunctionParts(rootNode); const ctx: Context = new ContextImpl(minify, params); diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index 7204375792..89b559b12d 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -8,14 +8,14 @@ describe('transpileFn', () => { it( 'fails when the input is not a function', dualTest((p) => { - expect(() => transpileFn(p('1 + 2'))).toThrow(); + expect(() => transpileFn(p('1 + 2'), false)).toThrow(); }), ); it( 'parses an empty arrow function', dualTest((p) => { - const { params, body, externalNames } = transpileFn(p('() => {}')); + const { params, body, externalNames } = transpileFn(p('() => {}'), false); expect(params).toStrictEqual([]); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[]]"`); @@ -26,7 +26,7 @@ describe('transpileFn', () => { it( 'parses an empty named function', dualTest((p) => { - const { params, body, externalNames } = transpileFn(p('function example() {}')); + const { params, body, externalNames } = transpileFn(p('function example() {}'), false); expect(params).toStrictEqual([]); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[]]"`); @@ -37,7 +37,7 @@ describe('transpileFn', () => { it( 'gathers external names', dualTest((p) => { - const { params, body, externalNames } = transpileFn(p('(a, b) => a + b - c')); + const { params, body, externalNames } = transpileFn(p('(a, b) => a + b - c'), false); expect(params).toStrictEqual([ { type: 'i', name: 'a' }, @@ -62,6 +62,7 @@ describe('transpileFn', () => { const a = 0; c = a + 2; }`), + false, ); expect(params).toStrictEqual([]); @@ -87,6 +88,7 @@ describe('transpileFn', () => { c = a + 2; } }`), + false, ); expect(params).toStrictEqual([]); @@ -105,7 +107,7 @@ describe('transpileFn', () => { it( 'treats the object as a possible external value when accessing a member', dualTest((p) => { - const { params, body, externalNames } = transpileFn(p('() => external.outside.prop')); + const { params, body, externalNames } = transpileFn(p('() => external.outside.prop'), false); expect(params).toStrictEqual([]); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,"external.outside.prop"]]]"`); @@ -125,6 +127,7 @@ describe('transpileFn', () => { p(`({ pos, a: b }) => { const x = pos.x; }`), + false, ); expect(params).toStrictEqual([ @@ -154,6 +157,7 @@ describe('transpileFn', () => { p(`(y, { pos, a: b }, {c, d}) => { const x = pos.x; }`), + false, ); expect(params).toStrictEqual([ @@ -194,7 +198,7 @@ describe('transpileFn', () => { ); it('handles TSNonNullExpression', () => { - const { body } = transpileFn(parseBabel('() => x!.y')); + const { body } = transpileFn(parseBabel('() => x!.y'), false); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[7,"x","y"]]]]"`); }); @@ -211,6 +215,7 @@ describe('transpileFn', () => { value += a; // refers to an external 'a' return value; }`), + false, ); expect(externalNames).toMatchInlineSnapshot(` @@ -233,6 +238,7 @@ describe('transpileFn', () => { value += a; // refers to an external 'a' return value; }`), + false, ); expect(externalNames).toMatchInlineSnapshot(` @@ -267,6 +273,7 @@ describe('transpileFn', () => { const l = ext; }`), + false, ); expect(externalNames).toMatchInlineSnapshot(` @@ -300,6 +307,7 @@ describe('transpileFn', () => { const a = ext; const b = ext; }`), + false, ); expect(externalNames).toMatchInlineSnapshot(` @@ -320,6 +328,7 @@ describe('transpileFn', () => { const c = ext.config.zero; const d = ext.config.multiplier; };`), + false, ); expect(externalNames).toMatchInlineSnapshot(` @@ -355,7 +364,7 @@ describe('transpileFn', () => { const lastProp = props.at(-1) as ClassProperty | acorn.PropertyDefinition; const fn = lastProp.value as Expression | acorn.Expression; - const { externalNames } = transpileFn(fn); + const { externalNames } = transpileFn(fn, false); expect(externalNames).toMatchInlineSnapshot(` Map { diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index ea55baccda..8df8855770 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -15,7 +15,7 @@ export interface Options { include?: FilterPattern; /** @default undefined */ - exclude?: FilterPattern; + exclude?: FilterPattern | undefined; /** @default undefined */ enforce?: 'post' | 'pre' | undefined; @@ -24,7 +24,7 @@ export interface Options { forceTgpuAlias?: string | undefined; /** @default true */ - autoNamingEnabled?: boolean | undefined; + autoNamingEnabled?: boolean; /** * Minify the generated AST. @@ -32,7 +32,7 @@ export interface Options { * * @default false */ - minify?: boolean | undefined; + minify?: boolean; /** * Skipping files that don't contain "typegpu", "tgpu" or "use gpu". @@ -114,7 +114,7 @@ export interface PluginState extends TransformMethods { * In Babel, options are assigned to the property `opts` on the plugin state. * We use this pattern everywhere for consistency. */ - opts: Options; + opts: Required; inUseGpuScope: boolean; } @@ -538,7 +538,7 @@ export const functionVisitor: TraverseOptions = { ArrowFunctionExpression: { enter(path, state) { if (containsUseGpuDirective(path.node)) { - fnNodeToTranspiledMap.set(path.node, transpileFn(path.node)); + fnNodeToTranspiledMap.set(path.node, transpileFn(path.node, this.opts.minify)); if (state.inUseGpuScope) { throw new Error(`Nesting 'use gpu' functions is not allowed`); } @@ -551,7 +551,7 @@ export const functionVisitor: TraverseOptions = { FunctionExpression: { enter(path, state) { if (containsUseGpuDirective(path.node)) { - fnNodeToTranspiledMap.set(path.node, transpileFn(path.node)); + fnNodeToTranspiledMap.set(path.node, transpileFn(path.node, this.opts.minify)); if (state.inUseGpuScope) { throw new Error(`Nesting 'use gpu' functions is not allowed`); } @@ -564,7 +564,7 @@ export const functionVisitor: TraverseOptions = { FunctionDeclaration: { enter(path, state) { if (containsUseGpuDirective(path.node)) { - fnNodeToTranspiledMap.set(path.node, transpileFn(path.node)); + fnNodeToTranspiledMap.set(path.node, transpileFn(path.node, this.opts.minify)); if (state.inUseGpuScope) { throw new Error(`Nesting 'use gpu' functions is not allowed`); } @@ -593,7 +593,7 @@ export const functionVisitor: TraverseOptions = { t.ArrowFunctionExpression | t.FunctionDeclaration | t.FunctionExpression >, getFunctionName(path.get('arguments.0')), - transpileFn(implementation), + transpileFn(implementation, this.opts.minify), ); } } diff --git a/packages/unplugin-typegpu/test/minification.test.ts b/packages/unplugin-typegpu/test/minification.test.ts new file mode 100644 index 0000000000..a186eb58ed --- /dev/null +++ b/packages/unplugin-typegpu/test/minification.test.ts @@ -0,0 +1,63 @@ +import { expect, test } from 'vitest'; +import { babelTransform, rollupTransform } from './transform.ts'; + +// No need to test the minification in-depth, as it is already tested in tinyest-for-wgsl. +const code = `\ +import { tgpu } from 'typegpu'; + +const external = { n: 1 } + +export const fn = (argument) => { + 'use gpu'; + const variable = 3; + return external.n + argument + variable; +}; +`; + +test('[BABEL] assigns minified metadata', () => { + expect(babelTransform(code, { minify: true })).toMatchInlineSnapshot(` + "import { tgpu } from 'typegpu'; + const external = { + n: 1 + }; + export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = argument => { + const variable = 3; + return __tsover_add(__tsover_add(external.n, argument), variable); + }, { + v: 2, + name: "fn", + ast: { + params: [{ + type: "i", + name: "a" + }], + body: [0, [[13, "b", [5, "3"]], [10, [1, [1, "c", "+", "a"], "+", "b"]]]] + }, + externals: { + "c": () => external.n + } + }) && $.f)({});" + `); +}); + +test('[ROLLUP] assigns minified metadata', async () => { + expect(await rollupTransform(code, { minify: true })).toMatchInlineSnapshot(` + "import 'typegpu'; + + const external = { n: 1 }; + + const fn = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = ((argument) => { + + const variable = 3; + return __tsover_add(__tsover_add(external.n, argument), variable); + }), { + v: 2, + name: "fn", + ast: {"params":[{"type":"i","name":"a"}],"body":[0,[[13,"b",[5,"3"]],[10,[1,[1,"c","+","a"],"+","b"]]]]}, + externals: {"c":() => external.n} + }) && $.f)({})); + + export { fn }; + " + `); +}); From e2e428b79b06844e18f7a95fe5d3af587f761611 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:18:10 +0200 Subject: [PATCH 14/29] Fix undefined handling --- .../tests/minification.test.ts | 30 ++++- .../tinyest-for-wgsl/tests/parsers.test.ts | 27 +++++ packages/typegpu/src/resolutionCtx.ts | 13 +- .../typegpu/tests/mutabilityTracking.test.ts | 18 +-- .../typegpu/tests/std/boolean/not.test.ts | 16 +-- .../typegpu/tests/tgsl/wgslGenerator.test.ts | 16 +-- .../test/minification.test.ts | 114 +++++++++++++----- 7 files changed, 169 insertions(+), 65 deletions(-) diff --git a/packages/tinyest-for-wgsl/tests/minification.test.ts b/packages/tinyest-for-wgsl/tests/minification.test.ts index 3aaf9de53a..7be7b5cc39 100644 --- a/packages/tinyest-for-wgsl/tests/minification.test.ts +++ b/packages/tinyest-for-wgsl/tests/minification.test.ts @@ -1,6 +1,3 @@ -import babel from '@babel/parser'; -import type { ClassDeclaration, ClassProperty, Expression, Node } from '@babel/types'; -import * as acorn from 'acorn'; import { describe, expect, it } from 'vitest'; import { transpileFn } from '../src/parsers.ts'; import { dualTest } from './helpers.ts'; @@ -68,6 +65,33 @@ describe('transpileFn', () => { }), ); + it( + 'handles weird identifiers', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p(`() => { + const a = undefined; + const b = Infinity; + const c = NaN; + }`), + true, + ); + + expect(params).toStrictEqual([]); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,"a","b"],[13,"c","d"],[13,"e","f"]]]"`, + ); + // These are identifiers, so they should be in externals. + expect(externalNames).toMatchInlineSnapshot(` + Map { + "b" => "undefined", + "d" => "Infinity", + "f" => "NaN", + } + `); + }), + ); + it( 'minifies parameters', dualTest((p) => { diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index 89b559b12d..601338f4a6 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -5,6 +5,33 @@ import { transpileFn } from '../src/parsers.ts'; import { dualTest, parseBabel } from './helpers.ts'; describe('transpileFn', () => { + it( + 'handles weird identifiers', + dualTest((p) => { + const { params, body, externalNames } = transpileFn( + p(`() => { + const a = undefined; + const b = Infinity; + const c = NaN; + }`), + false, + ); + + expect(params).toStrictEqual([]); + expect(JSON.stringify(body)).toMatchInlineSnapshot( + `"[0,[[13,"a","undefined"],[13,"b","Infinity"],[13,"c","NaN"]]]"`, + ); + // These are identifiers, so they should be in externals. + expect(externalNames).toMatchInlineSnapshot(` + Map { + "undefined" => "undefined", + "Infinity" => "Infinity", + "NaN" => "NaN", + } + `); + }), + ); + it( 'fails when the input is not a function', dualTest((p) => { diff --git a/packages/typegpu/src/resolutionCtx.ts b/packages/typegpu/src/resolutionCtx.ts index 4d234a8eb6..b5a6016ad9 100644 --- a/packages/typegpu/src/resolutionCtx.ts +++ b/packages/typegpu/src/resolutionCtx.ts @@ -202,17 +202,14 @@ class ItemStateStackImpl implements ItemStateStack { return access(); } - const external = layer.externalMap[id]; - if (isNamable(external) && getName(external) === undefined) { - setName(external, id.replaceAll('.', '_')); - } - - if (external !== undefined && external !== null) { + if (id in layer.externalMap) { + const external = layer.externalMap[id]; + if (isNamable(external) && getName(external) === undefined) { + setName(external, id.replaceAll('.', '_')); + } return coerceToSnippet(external); } - // Since functions cannot access resources from the calling scope, we - // return early here. return undefined; } diff --git a/packages/typegpu/tests/mutabilityTracking.test.ts b/packages/typegpu/tests/mutabilityTracking.test.ts index 513dad90cd..d31be32e98 100644 --- a/packages/typegpu/tests/mutabilityTracking.test.ts +++ b/packages/typegpu/tests/mutabilityTracking.test.ts @@ -350,15 +350,15 @@ describe('mutability tracking', () => { const resolved = tgpu.resolve([fn]); expect(resolved).toMatchInlineSnapshot(` - "fn item(arg: vec4u) -> u32 { - let a = arg; - { - var a_1 = arg; - a_1.x = 2u; - } - return a.x; - }" - `); + "fn item(arg: vec4u) -> u32 { + let a = arg; + { + var a_1 = arg; + a_1.x = 2u; + } + return a.x; + }" + `); expect(resolved).toContain('let a = arg'); expect(resolved).toContain('var a_1 = arg'); }); diff --git a/packages/typegpu/tests/std/boolean/not.test.ts b/packages/typegpu/tests/std/boolean/not.test.ts index 5a7fb2e57a..80fa45395a 100644 --- a/packages/typegpu/tests/std/boolean/not.test.ts +++ b/packages/typegpu/tests/std/boolean/not.test.ts @@ -60,10 +60,10 @@ describe('not', () => { return not(v); }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn(v: i32) -> bool { - return !bool(v); - }" - `); + "fn testFn(v: i32) -> bool { + return !bool(v); + }" + `); }); it('generates correct WGSL on a boolean vector runtime-known argument', () => { @@ -178,10 +178,10 @@ describe('not', () => { return not(v); }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn(v: i32) -> bool { - return !bool(v); - }" - `); + "fn testFn(v: i32) -> bool { + return !bool(v); + }" + `); }); it('generates correct WGSL on a boolean vector runtime-known argument', () => { diff --git a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts index 828f40646c..765b9c16b7 100644 --- a/packages/typegpu/tests/tgsl/wgslGenerator.test.ts +++ b/packages/typegpu/tests/tgsl/wgslGenerator.test.ts @@ -1771,10 +1771,10 @@ describe('wgslGenerator', () => { }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn(b: bool) -> bool { - return !b; - }" - `); + "fn testFn(b: bool) -> bool { + return !b; + }" + `); }); it('handles unary operator `!` on numeric runtime-known operand', () => { @@ -1786,10 +1786,10 @@ describe('wgslGenerator', () => { }); expect(tgpu.resolve([testFn])).toMatchInlineSnapshot(` - "fn testFn(n: i32) -> bool { - return !bool(n); - }" - `); + "fn testFn(n: i32) -> bool { + return !bool(n); + }" + `); }); it('handles unary operator `!` on non-primitive values', ({ root }) => { diff --git a/packages/unplugin-typegpu/test/minification.test.ts b/packages/unplugin-typegpu/test/minification.test.ts index a186eb58ed..12db71faa3 100644 --- a/packages/unplugin-typegpu/test/minification.test.ts +++ b/packages/unplugin-typegpu/test/minification.test.ts @@ -1,21 +1,23 @@ import { expect, test } from 'vitest'; import { babelTransform, rollupTransform } from './transform.ts'; +import { describe } from 'node:test'; // No need to test the minification in-depth, as it is already tested in tinyest-for-wgsl. -const code = `\ -import { tgpu } from 'typegpu'; +describe('minification', () => { + describe('assigns minified metadata', () => { + const code = `\ + import { tgpu } from 'typegpu'; -const external = { n: 1 } + const external = { n: 1 } -export const fn = (argument) => { - 'use gpu'; - const variable = 3; - return external.n + argument + variable; -}; -`; + export const fn = (argument) => { + 'use gpu'; + const variable = 3; + return external.n + argument + variable; + };`; -test('[BABEL] assigns minified metadata', () => { - expect(babelTransform(code, { minify: true })).toMatchInlineSnapshot(` + test('[BABEL]', () => { + expect(babelTransform(code, { minify: true })).toMatchInlineSnapshot(` "import { tgpu } from 'typegpu'; const external = { n: 1 @@ -38,26 +40,80 @@ test('[BABEL] assigns minified metadata', () => { } }) && $.f)({});" `); -}); + }); -test('[ROLLUP] assigns minified metadata', async () => { - expect(await rollupTransform(code, { minify: true })).toMatchInlineSnapshot(` - "import 'typegpu'; + test('[ROLLUP]', async () => { + expect(await rollupTransform(code, { minify: true })).toMatchInlineSnapshot(` + "import 'typegpu'; - const external = { n: 1 }; + const external = { n: 1 }; - const fn = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = ((argument) => { - - const variable = 3; - return __tsover_add(__tsover_add(external.n, argument), variable); - }), { - v: 2, - name: "fn", - ast: {"params":[{"type":"i","name":"a"}],"body":[0,[[13,"b",[5,"3"]],[10,[1,[1,"c","+","a"],"+","b"]]]]}, - externals: {"c":() => external.n} - }) && $.f)({})); - - export { fn }; - " + const fn = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = ((argument) => { + + const variable = 3; + return __tsover_add(__tsover_add(external.n, argument), variable); + }), { + v: 2, + name: "fn", + ast: {"params":[{"type":"i","name":"a"}],"body":[0,[[13,"b",[5,"3"]],[10,[1,[1,"c","+","a"],"+","b"]]]]}, + externals: {"c":() => external.n} + }) && $.f)({})); + + export { fn }; + " + `); + }); + }); + + describe('weird identifiers', () => { + const code = ` + import { tgpu } from 'typegpu'; + + export const fn = () => { + 'use gpu'; + const a = undefined; + const b = Infinity; + const c = NaN; + }`; + + test('[BABEL]', () => { + expect(babelTransform(code, { minify: true })).toMatchInlineSnapshot(` + "import { tgpu } from 'typegpu'; + export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = () => { + const a = undefined; + const b = Infinity; + const c = NaN; + }, { + v: 2, + name: "fn", + ast: { + params: [], + body: [0, [[13, "a", "b"], [13, "c", "d"], [13, "e", "f"]]] + }, + externals: { + "b": () => undefined, + "d": () => Infinity, + "f": () => NaN + } + }) && $.f)({});" `); + }); + + test('[ROLLUP]', async () => { + expect(await rollupTransform(code, { minify: true })).toMatchInlineSnapshot(` + "import 'typegpu'; + + const fn = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = (() => { + }), { + v: 2, + name: "fn", + ast: {"params":[],"body":[0,[[13,"a","b"],[13,"c","d"],[13,"e","f"]]]}, + externals: {"b":() => undefined,"d":() => Infinity,"f":() => NaN} + }) && $.f)({})); + + export { fn }; + " + `); + }); + }); }); From 071c869a7e53fa25045dd62b74b447c2a6977793 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:57:10 +0200 Subject: [PATCH 15/29] Add obfuscation stub to unplugin --- packages/unplugin-typegpu/src/core/common.ts | 20 +- .../unplugin-typegpu/src/core/minifier.ts | 122 ++++++++++++ .../unplugin-typegpu/src/core/obfuscate.ts | 178 ++++++++++++++++++ .../test/minification.test.ts | 82 ++++---- 4 files changed, 357 insertions(+), 45 deletions(-) create mode 100644 packages/unplugin-typegpu/src/core/minifier.ts create mode 100644 packages/unplugin-typegpu/src/core/obfuscate.ts diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index 8df8855770..6dac63e4c1 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -3,6 +3,7 @@ import type { NodePath, TraverseOptions } from '@babel/traverse'; import type { FilterPattern } from 'unplugin'; import MagicString from 'magic-string'; import { transpileFn } from 'tinyest-for-wgsl'; +import { obfuscate } from './obfuscate.ts'; /** * Each breaking change to the metadata format requires a bump to this number. @@ -480,6 +481,17 @@ function functionOnExit( path.skip(); } +function transpile( + rootNode: Parameters[0], + obf: boolean, +): ReturnType { + const result = transpileFn(rootNode, false); + if (obf) { + return obfuscate(result); + } + return result; +} + export const functionVisitor: TraverseOptions = { ImportDeclaration(path, state) { gatherTgpuAliases(state, path.node); @@ -538,7 +550,7 @@ export const functionVisitor: TraverseOptions = { ArrowFunctionExpression: { enter(path, state) { if (containsUseGpuDirective(path.node)) { - fnNodeToTranspiledMap.set(path.node, transpileFn(path.node, this.opts.minify)); + fnNodeToTranspiledMap.set(path.node, transpile(path.node, this.opts.minify)); if (state.inUseGpuScope) { throw new Error(`Nesting 'use gpu' functions is not allowed`); } @@ -551,7 +563,7 @@ export const functionVisitor: TraverseOptions = { FunctionExpression: { enter(path, state) { if (containsUseGpuDirective(path.node)) { - fnNodeToTranspiledMap.set(path.node, transpileFn(path.node, this.opts.minify)); + fnNodeToTranspiledMap.set(path.node, transpile(path.node, this.opts.minify)); if (state.inUseGpuScope) { throw new Error(`Nesting 'use gpu' functions is not allowed`); } @@ -564,7 +576,7 @@ export const functionVisitor: TraverseOptions = { FunctionDeclaration: { enter(path, state) { if (containsUseGpuDirective(path.node)) { - fnNodeToTranspiledMap.set(path.node, transpileFn(path.node, this.opts.minify)); + fnNodeToTranspiledMap.set(path.node, transpile(path.node, this.opts.minify)); if (state.inUseGpuScope) { throw new Error(`Nesting 'use gpu' functions is not allowed`); } @@ -593,7 +605,7 @@ export const functionVisitor: TraverseOptions = { t.ArrowFunctionExpression | t.FunctionDeclaration | t.FunctionExpression >, getFunctionName(path.get('arguments.0')), - transpileFn(implementation, this.opts.minify), + transpile(implementation, this.opts.minify), ); } } diff --git a/packages/unplugin-typegpu/src/core/minifier.ts b/packages/unplugin-typegpu/src/core/minifier.ts new file mode 100644 index 0000000000..aaaf2e4164 --- /dev/null +++ b/packages/unplugin-typegpu/src/core/minifier.ts @@ -0,0 +1,122 @@ +export interface Minifier { + /** + * If `name` wasn't minified before, it gives it a new minified name. + * Then, returns the minified version of `name`. + */ + minify(name: string): string; + /** + * Returns the minified version of `name` if it exists, otherwise returns undefined. + */ + getIfMinified(name: string): string | undefined; +} + +export class MinifierNullImpl implements Minifier { + minify(name: string): string { + return name; + } + getIfMinified(name: string) { + return name; + // TODO: reconsider, this may backfire + } +} + +/** + * Generates all strings consisting of lowercase letters of the given length. + */ +function* combinationGenerator(length: number): Generator { + if (length === 0) { + yield ''; + return; + } + + for (let i = 97 /* ASCII a */; i <= 122 /* ASCII z */; i++) { + for (const name of combinationGenerator(length - 1)) { + yield `${String.fromCharCode(i)}${name}`; + } + } +} + +/** + * Generates fresh minified names, avoids forbidden tokens. + */ +function* freshNameGenerator(): Generator { + for (let i = 1; i <= 4; i++) { + for (const name of combinationGenerator(i)) { + if (!bannedTokens.has(name)) { + yield name; + } + } + } + throw new Error('Too many variable names!'); +} + +export class MinifierImpl implements Minifier { + #nameMap: Map = new Map(); + #nameGenerator: Generator = freshNameGenerator(); + + #generateFreshName(): string { + return this.#nameGenerator.next().value; + } + + minify(name: string): string { + let minifiedName = this.#nameMap.get(name); + if (!minifiedName) { + minifiedName = this.#generateFreshName(); + this.#nameMap.set(name, minifiedName); + } + + return minifiedName; + } + + getIfMinified(name: string) { + return this.#nameMap.get(name); + } +} + +// TODO: docs +// TODO: function names +export const bannedTokens = new Set([ + 'case', + 'else', + 'fn', + 'for', + 'if', + 'let', + 'loop', + 'true', + 'var', + 'NULL', + 'Self', + 'as', + 'asm', + 'auto', + 'cast', + 'do', + 'enum', + 'from', + 'get', + 'goto', + 'impl', + 'lowp', + 'meta', + 'mod', + 'move', + 'mut', + 'new', + 'nil', + 'null', + 'of', + 'pass', + 'priv', + 'pub', + 'ref', + 'self', + 'set', + 'std', + 'this', + 'try', + 'type', + 'use', + 'wgsl', + 'with', +]); diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts new file mode 100644 index 0000000000..2849aa11e4 --- /dev/null +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -0,0 +1,178 @@ +import type { TranspilationResult } from '../../../tinyest-for-wgsl/src/types.ts'; +import { MinifierImpl, MinifierNullImpl, type Minifier } from './minifier.ts'; +import * as tinyest from 'tinyest'; +const { NodeTypeCatalog: NODE } = tinyest; + +class Context { + ignoreMinificationDepth = 0; + minifier: Minifier; + + constructor() { + this.minifier = new MinifierNullImpl(); + } +} + +export function obfuscate(fn: TranspilationResult) { + return fn; + + const ctx = new Context(); + + const params = fn.params.map((param) => { + if (param.type === 'i') { + return { ...param, name: ctx.minifier.minify(param.name) }; + } + return { + ...param, + props: param.props.map((prop) => ({ ...prop, alias: ctx.minifier.minify(prop.alias) })), + }; + }); + + const body = runOnTinyest(ctx, fn.body); + + const externalNames = new Map(); + fn.externalNames.forEach((key, value) => externalNames.set(ctx.minifier.minify(key), value)); + + return { params: params, body: body, externalNames }; +} + +// No default fallback for nodes like 'continue' and 'break' so that types will warn us when a new node is added. +const visitors = { + block(ctx: Context, node: tinyest.Block) { + return [NODE.block, node[1].map((node) => runOnTinyest(ctx, node))]; + }, + binaryExpr(ctx: Context, node: tinyest.BinaryExpression) { + return [NODE.binaryExpr, runOnTinyest(ctx, node[1]), node[2], runOnTinyest(ctx, node[3])]; + }, + assignmentExpr(ctx: Context, node: tinyest.AssignmentExpression) { + return [NODE.assignmentExpr, runOnTinyest(ctx, node[1]), node[2], runOnTinyest(ctx, node[3])]; + }, + logicalExpr(ctx: Context, node: tinyest.LogicalExpression) { + return [NODE.logicalExpr, runOnTinyest(ctx, node[1]), node[2], runOnTinyest(ctx, node[3])]; + }, + unaryExpr(ctx: Context, node: tinyest.UnaryExpression) { + return [NODE.unaryExpr, node[1], runOnTinyest(ctx, node[2])]; + }, + numericLiteral(_ctx: Context, node: tinyest.Num) { + return [NODE.numericLiteral, node[1]]; + }, + call(ctx: Context, node: tinyest.Call) { + return [NODE.call, runOnTinyest(ctx, node[1]), node[2].map((node) => runOnTinyest(ctx, node))]; + }, + memberAccess(ctx: Context, node: tinyest.MemberAccess) { + return [NODE.memberAccess, runOnTinyest(ctx, node[1]), node[2]]; + }, + indexAccess(ctx: Context, node: tinyest.IndexAccess) { + return [NODE.indexAccess, runOnTinyest(ctx, node[1]), runOnTinyest(ctx, node[2])]; + }, + return(ctx: Context, node: tinyest.Return) { + return node.length === 1 ? [NODE.return] : [NODE.return, runOnTinyest(ctx, node[1])]; + }, + if(ctx: Context, node: tinyest.If) { + return node.length === 3 + ? [NODE.if, runOnTinyest(ctx, node[1]), runOnTinyest(ctx, node[2])] + : [ + NODE.if, + runOnTinyest(ctx, node[1]), + runOnTinyest(ctx, node[2]), + runOnTinyest(ctx, node[3]), + ]; + }, + let(ctx: Context, node: tinyest.Let) { + return node.length === 2 + ? [NODE.let, node[1]] + : [NODE.let, node[1], runOnTinyest(ctx, node[2])]; + }, + const(ctx: Context, node: tinyest.Const) { + return node.length === 2 + ? [NODE.const, node[1]] + : [NODE.const, node[1], runOnTinyest(ctx, node[2])]; + }, + for(ctx: Context, node: tinyest.For) { + return [ + NODE.for, + runOnTinyest(ctx, node[1]), + runOnTinyest(ctx, node[2]), + runOnTinyest(ctx, node[3]), + runOnTinyest(ctx, node[4]), + ]; + }, + while(ctx: Context, node: tinyest.While) { + return [NODE.while, runOnTinyest(ctx, node[1]), runOnTinyest(ctx, node[2])]; + }, + continue(_ctx: Context, _node: tinyest.Continue) { + return [NODE.continue]; + }, + break(_ctx: Context, _node: tinyest.Break) { + return [NODE.break]; + }, + forOf(ctx: Context, node: tinyest.ForOf) { + return [ + NODE.forOf, + runOnTinyest(ctx, node[1]), + runOnTinyest(ctx, node[2]), + runOnTinyest(ctx, node[3]), + ]; + }, + arrayExpr(ctx: Context, node: tinyest.ArrayExpression) { + return [NODE.arrayExpr, node[1].map((node) => runOnTinyest(ctx, node))]; + }, + preUpdate(ctx: Context, node: tinyest.PreUpdate) { + return [NODE.preUpdate, node[1], runOnTinyest(ctx, node[2])]; + }, + postUpdate(ctx: Context, node: tinyest.PostUpdate) { + return [NODE.postUpdate, node[1], runOnTinyest(ctx, node[2])]; + }, + stringLiteral(_ctx: Context, node: tinyest.Str) { + return [NODE.stringLiteral, node[1]]; + }, + objectExpr(ctx: Context, node: tinyest.ObjectExpression) { + return [ + NODE.objectExpr, + Object.fromEntries( + Object.entries(node[1]).map(([key, value]) => [key, runOnTinyest(ctx, value)]), + ), + ]; + }, + conditionalExpr(ctx: Context, node: tinyest.ConditionalExpression) { + return [ + NODE.conditionalExpr, + runOnTinyest(ctx, node[1]), + runOnTinyest(ctx, node[2]), + runOnTinyest(ctx, node[3]), + ]; + }, +} as const satisfies { + [N in keyof typeof NODE]: ( + ctx: Context, + node: Extract, + ) => tinyest.AnyNode; +}; + +const nodeIdToName = new Map(Object.entries(NODE).map(([key, value]) => [value, key])) as Map< + number, + keyof typeof NODE +>; + +function runOnTinyest(ctx: Context, node: T): T { + if (node === null) { + return node; + } + + if (typeof node === 'string') { + return node; + } + + if (typeof node === 'boolean') { + return node; + } + + const nodeName: keyof typeof visitors | undefined = nodeIdToName.get(node[0]); + if (nodeName === undefined) { + throw new Error('AAA'); + } + const visitor = visitors[nodeName] as unknown as ((ctx: Context, node: T) => T) | undefined; + if (!visitor) { + throw new Error('BBB'); + } + return visitor(ctx, node); +} diff --git a/packages/unplugin-typegpu/test/minification.test.ts b/packages/unplugin-typegpu/test/minification.test.ts index 12db71faa3..45755dbeb3 100644 --- a/packages/unplugin-typegpu/test/minification.test.ts +++ b/packages/unplugin-typegpu/test/minification.test.ts @@ -18,28 +18,28 @@ describe('minification', () => { test('[BABEL]', () => { expect(babelTransform(code, { minify: true })).toMatchInlineSnapshot(` - "import { tgpu } from 'typegpu'; - const external = { - n: 1 - }; - export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = argument => { - const variable = 3; - return __tsover_add(__tsover_add(external.n, argument), variable); - }, { - v: 2, - name: "fn", - ast: { - params: [{ - type: "i", - name: "a" - }], - body: [0, [[13, "b", [5, "3"]], [10, [1, [1, "c", "+", "a"], "+", "b"]]]] - }, - externals: { - "c": () => external.n - } - }) && $.f)({});" - `); + "import { tgpu } from 'typegpu'; + const external = { + n: 1 + }; + export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = argument => { + const variable = 3; + return __tsover_add(__tsover_add(external.n, argument), variable); + }, { + v: 2, + name: "fn", + ast: { + params: [{ + type: "i", + name: "a" + }], + body: [0, [[13, "b", [5, "3"]], [10, [1, [1, "c", "+", "a"], "+", "b"]]]] + }, + externals: { + "c": () => external.n + } + }) && $.f)({});" + `); }); test('[ROLLUP]', async () => { @@ -78,25 +78,25 @@ describe('minification', () => { test('[BABEL]', () => { expect(babelTransform(code, { minify: true })).toMatchInlineSnapshot(` - "import { tgpu } from 'typegpu'; - export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = () => { - const a = undefined; - const b = Infinity; - const c = NaN; - }, { - v: 2, - name: "fn", - ast: { - params: [], - body: [0, [[13, "a", "b"], [13, "c", "d"], [13, "e", "f"]]] - }, - externals: { - "b": () => undefined, - "d": () => Infinity, - "f": () => NaN - } - }) && $.f)({});" - `); + "import { tgpu } from 'typegpu'; + export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = () => { + const a = undefined; + const b = Infinity; + const c = NaN; + }, { + v: 2, + name: "fn", + ast: { + params: [], + body: [0, [[13, "a", "b"], [13, "c", "d"], [13, "e", "f"]]] + }, + externals: { + "b": () => undefined, + "d": () => Infinity, + "f": () => NaN + } + }) && $.f)({});" + `); }); test('[ROLLUP]', async () => { From 8e2b3b0d2f7fe55977f70cfd7b2690c040c2042d Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:49:08 +0200 Subject: [PATCH 16/29] Implement obfuscate --- .../unplugin-typegpu/src/core/obfuscate.ts | 89 ++++++++----------- 1 file changed, 37 insertions(+), 52 deletions(-) diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index 2849aa11e4..e50d93743f 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -4,17 +4,14 @@ import * as tinyest from 'tinyest'; const { NodeTypeCatalog: NODE } = tinyest; class Context { - ignoreMinificationDepth = 0; minifier: Minifier; constructor() { - this.minifier = new MinifierNullImpl(); + this.minifier = new MinifierImpl(); } } export function obfuscate(fn: TranspilationResult) { - return fn; - const ctx = new Context(); const params = fn.params.map((param) => { @@ -27,77 +24,66 @@ export function obfuscate(fn: TranspilationResult) { }; }); - const body = runOnTinyest(ctx, fn.body); + const body = obf(ctx, fn.body); const externalNames = new Map(); fn.externalNames.forEach((key, value) => externalNames.set(ctx.minifier.minify(key), value)); - return { params: params, body: body, externalNames }; + return { params, body, externalNames }; } // No default fallback for nodes like 'continue' and 'break' so that types will warn us when a new node is added. const visitors = { block(ctx: Context, node: tinyest.Block) { - return [NODE.block, node[1].map((node) => runOnTinyest(ctx, node))]; + return [NODE.block, node[1].map((node) => obf(ctx, node))]; }, binaryExpr(ctx: Context, node: tinyest.BinaryExpression) { - return [NODE.binaryExpr, runOnTinyest(ctx, node[1]), node[2], runOnTinyest(ctx, node[3])]; + return [NODE.binaryExpr, obf(ctx, node[1]), node[2], obf(ctx, node[3])]; }, assignmentExpr(ctx: Context, node: tinyest.AssignmentExpression) { - return [NODE.assignmentExpr, runOnTinyest(ctx, node[1]), node[2], runOnTinyest(ctx, node[3])]; + return [NODE.assignmentExpr, obf(ctx, node[1]), node[2], obf(ctx, node[3])]; }, logicalExpr(ctx: Context, node: tinyest.LogicalExpression) { - return [NODE.logicalExpr, runOnTinyest(ctx, node[1]), node[2], runOnTinyest(ctx, node[3])]; + return [NODE.logicalExpr, obf(ctx, node[1]), node[2], obf(ctx, node[3])]; }, unaryExpr(ctx: Context, node: tinyest.UnaryExpression) { - return [NODE.unaryExpr, node[1], runOnTinyest(ctx, node[2])]; + return [NODE.unaryExpr, node[1], obf(ctx, node[2])]; }, numericLiteral(_ctx: Context, node: tinyest.Num) { return [NODE.numericLiteral, node[1]]; }, call(ctx: Context, node: tinyest.Call) { - return [NODE.call, runOnTinyest(ctx, node[1]), node[2].map((node) => runOnTinyest(ctx, node))]; + return [NODE.call, obf(ctx, node[1]), node[2].map((node) => obf(ctx, node))]; }, memberAccess(ctx: Context, node: tinyest.MemberAccess) { - return [NODE.memberAccess, runOnTinyest(ctx, node[1]), node[2]]; + return [NODE.memberAccess, obf(ctx, node[1]), /* intentionally omitted */ node[2]]; }, indexAccess(ctx: Context, node: tinyest.IndexAccess) { - return [NODE.indexAccess, runOnTinyest(ctx, node[1]), runOnTinyest(ctx, node[2])]; + return [NODE.indexAccess, obf(ctx, node[1]), obf(ctx, node[2])]; }, return(ctx: Context, node: tinyest.Return) { - return node.length === 1 ? [NODE.return] : [NODE.return, runOnTinyest(ctx, node[1])]; + return node.length === 1 ? [NODE.return] : [NODE.return, obf(ctx, node[1])]; }, if(ctx: Context, node: tinyest.If) { return node.length === 3 - ? [NODE.if, runOnTinyest(ctx, node[1]), runOnTinyest(ctx, node[2])] - : [ - NODE.if, - runOnTinyest(ctx, node[1]), - runOnTinyest(ctx, node[2]), - runOnTinyest(ctx, node[3]), - ]; + ? [NODE.if, obf(ctx, node[1]), obf(ctx, node[2])] + : [NODE.if, obf(ctx, node[1]), obf(ctx, node[2]), obf(ctx, node[3])]; }, let(ctx: Context, node: tinyest.Let) { return node.length === 2 - ? [NODE.let, node[1]] - : [NODE.let, node[1], runOnTinyest(ctx, node[2])]; + ? [NODE.let, obf(ctx, node[1])] + : [NODE.let, obf(ctx, node[1]), obf(ctx, node[2])]; }, const(ctx: Context, node: tinyest.Const) { return node.length === 2 - ? [NODE.const, node[1]] - : [NODE.const, node[1], runOnTinyest(ctx, node[2])]; + ? [NODE.const, obf(ctx, node[1])] + : [NODE.const, obf(ctx, node[1]), obf(ctx, node[2])]; }, for(ctx: Context, node: tinyest.For) { - return [ - NODE.for, - runOnTinyest(ctx, node[1]), - runOnTinyest(ctx, node[2]), - runOnTinyest(ctx, node[3]), - runOnTinyest(ctx, node[4]), - ]; + return [NODE.for, obf(ctx, node[1]), obf(ctx, node[2]), obf(ctx, node[3]), obf(ctx, node[4])]; }, while(ctx: Context, node: tinyest.While) { - return [NODE.while, runOnTinyest(ctx, node[1]), runOnTinyest(ctx, node[2])]; + return [NODE.while, obf(ctx, node[1]), obf(ctx, node[2])]; }, continue(_ctx: Context, _node: tinyest.Continue) { return [NODE.continue]; @@ -106,21 +92,16 @@ const visitors = { return [NODE.break]; }, forOf(ctx: Context, node: tinyest.ForOf) { - return [ - NODE.forOf, - runOnTinyest(ctx, node[1]), - runOnTinyest(ctx, node[2]), - runOnTinyest(ctx, node[3]), - ]; + return [NODE.forOf, obf(ctx, node[1]), obf(ctx, node[2]), obf(ctx, node[3])]; }, arrayExpr(ctx: Context, node: tinyest.ArrayExpression) { - return [NODE.arrayExpr, node[1].map((node) => runOnTinyest(ctx, node))]; + return [NODE.arrayExpr, node[1].map((node) => obf(ctx, node))]; }, preUpdate(ctx: Context, node: tinyest.PreUpdate) { - return [NODE.preUpdate, node[1], runOnTinyest(ctx, node[2])]; + return [NODE.preUpdate, node[1], obf(ctx, node[2])]; }, postUpdate(ctx: Context, node: tinyest.PostUpdate) { - return [NODE.postUpdate, node[1], runOnTinyest(ctx, node[2])]; + return [NODE.postUpdate, node[1], obf(ctx, node[2])]; }, stringLiteral(_ctx: Context, node: tinyest.Str) { return [NODE.stringLiteral, node[1]]; @@ -129,17 +110,15 @@ const visitors = { return [ NODE.objectExpr, Object.fromEntries( - Object.entries(node[1]).map(([key, value]) => [key, runOnTinyest(ctx, value)]), + Object.entries(node[1]).map(([key, value]) => [ + /* intentionally omitted */ key, + obf(ctx, value), + ]), ), ]; }, conditionalExpr(ctx: Context, node: tinyest.ConditionalExpression) { - return [ - NODE.conditionalExpr, - runOnTinyest(ctx, node[1]), - runOnTinyest(ctx, node[2]), - runOnTinyest(ctx, node[3]), - ]; + return [NODE.conditionalExpr, obf(ctx, node[1]), obf(ctx, node[2]), obf(ctx, node[3])]; }, } as const satisfies { [N in keyof typeof NODE]: ( @@ -153,13 +132,19 @@ const nodeIdToName = new Map(Object.entries(NODE).map(([key, value]) => [value, keyof typeof NODE >; -function runOnTinyest(ctx: Context, node: T): T { +/** + * Traverses the AST and generates a new one that is obfuscated. + * Copies old AST when identifiers cannot appear in a subtree, + * e.g. in a member access property, or for operator ('=', '<', ...) nodes. + */ +function obf(ctx: Context, node: T): T { if (node === null) { return node; } if (typeof node === 'string') { - return node; + // If we got here, then this identifier should be minified. + return ctx.minifier.minify(node) as T; } if (typeof node === 'boolean') { From 76cebf2df629109922ef03aab396b3bb4ed1d329 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:10:55 +0200 Subject: [PATCH 17/29] Revert parsers to original state --- packages/tinyest-for-wgsl/src/externals.ts | 6 +- packages/tinyest-for-wgsl/src/minifier.ts | 110 ------------------- packages/tinyest-for-wgsl/src/parsers.ts | 82 ++++++-------- packages/tinyest-for-wgsl/src/types.ts | 21 +--- packages/unplugin-typegpu/src/core/common.ts | 17 ++- 5 files changed, 46 insertions(+), 190 deletions(-) delete mode 100644 packages/tinyest-for-wgsl/src/minifier.ts diff --git a/packages/tinyest-for-wgsl/src/externals.ts b/packages/tinyest-for-wgsl/src/externals.ts index 633cc8bf2b..479ba914d4 100644 --- a/packages/tinyest-for-wgsl/src/externals.ts +++ b/packages/tinyest-for-wgsl/src/externals.ts @@ -1,11 +1,7 @@ import type { Context, JsNode } from './types.ts'; function isDeclared(ctx: Context, name: string) { - const minifiedName = ctx.minifier.getIfMinified(name); - if (!minifiedName) { - return false; - } - return ctx.stack.some((scope) => scope.declaredNames.includes(minifiedName)); + return ctx.stack.some((scope) => scope.declaredNames.includes(name)); } /** diff --git a/packages/tinyest-for-wgsl/src/minifier.ts b/packages/tinyest-for-wgsl/src/minifier.ts deleted file mode 100644 index 36eda2990c..0000000000 --- a/packages/tinyest-for-wgsl/src/minifier.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { Minifier } from './types.ts'; - -export class MinifierNullImpl implements Minifier { - minify(name: string): string { - return name; - } - getIfMinified(name: string) { - return name; - // TODO: reconsider, this may backfire - } -} - -/** - * Generates all strings consisting of lowercase letters of the given length. - */ -function* combinationGenerator(length: number): Generator { - if (length === 0) { - yield ''; - return; - } - - for (let i = 97 /* ASCII a */; i <= 122 /* ASCII z */; i++) { - for (const name of combinationGenerator(length - 1)) { - yield `${String.fromCharCode(i)}${name}`; - } - } -} - -/** - * Generates fresh minified names, avoids forbidden tokens. - */ -function* freshNameGenerator(): Generator { - for (let i = 1; i <= 4; i++) { - for (const name of combinationGenerator(i)) { - if (!bannedTokens.has(name)) { - yield name; - } - } - } - throw new Error('Too many variable names!'); -} - -export class MinifierImpl implements Minifier { - #nameMap: Map = new Map(); - #nameGenerator: Generator = freshNameGenerator(); - - #generateFreshName(): string { - return this.#nameGenerator.next().value; - } - - minify(name: string): string { - let minifiedName = this.#nameMap.get(name); - if (!minifiedName) { - minifiedName = this.#generateFreshName(); - this.#nameMap.set(name, minifiedName); - } - - return minifiedName; - } - - getIfMinified(name: string) { - return this.#nameMap.get(name); - } -} - -export const bannedTokens = new Set([ - 'case', - 'else', - 'fn', - 'for', - 'if', - 'let', - 'loop', - 'true', - 'var', - 'NULL', - 'Self', - 'as', - 'asm', - 'auto', - 'cast', - 'do', - 'enum', - 'from', - 'get', - 'goto', - 'impl', - 'lowp', - 'meta', - 'mod', - 'move', - 'mut', - 'new', - 'nil', - 'null', - 'of', - 'pass', - 'priv', - 'pub', - 'ref', - 'self', - 'set', - 'std', - 'this', - 'try', - 'type', - 'use', - 'wgsl', - 'with', -]); diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index ec0ae6e748..69299ac36d 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -2,9 +2,8 @@ import type * as babel from '@babel/types'; import type * as acorn from 'acorn'; import * as tinyest from 'tinyest'; import { FuncParameterType } from 'tinyest'; -import type { Context, JsNode, Minifier, Scope, TranspilationResult } from './types.ts'; +import type { Context, JsNode, TranspilationResult } from './types.ts'; import { tryFindExternalChain } from './externals.ts'; -import { MinifierImpl, MinifierNullImpl } from './minifier.ts'; const { NodeTypeCatalog: NODE } = tinyest; @@ -53,10 +52,7 @@ const Transpilers: Partial<{ : [NODE.return], Identifier(ctx, node) { - if (ctx.ignoreMinificationDepth > 0) { - return node.name; - } - return ctx.minifier.minify(node.name); + return node.name; }, ThisExpression() { @@ -98,9 +94,7 @@ const Transpilers: Partial<{ // If the property is not computed, we don't want to register identifiers as external. ctx.ignoreExternalDepth++; - ctx.ignoreMinificationDepth++; const property = transpile(ctx, node.property) as tinyest.Expression; - ctx.ignoreMinificationDepth--; ctx.ignoreExternalDepth--; if (typeof property !== 'string') { @@ -237,12 +231,10 @@ const Transpilers: Partial<{ } ctx.ignoreExternalDepth++; - ctx.ignoreMinificationDepth++; const key = prop.key.type === 'Identifier' ? (transpile(ctx, prop.key) as string) : String(prop.key.value); - ctx.ignoreMinificationDepth--; ctx.ignoreExternalDepth--; const value = transpile(ctx, prop.value) as tinyest.Expression; @@ -309,9 +301,8 @@ function transpile(ctx: Context, node: JsNode): tinyest.AnyNode { // add it to externals and swap the AST node for an identifier. const externalChain = tryFindExternalChain(ctx, node); if (externalChain) { - const minified = ctx.minifier.minify(externalChain); - ctx.externalNames.set(minified, externalChain); - return minified; + ctx.externalNames.add(externalChain); + return externalChain; } } @@ -417,61 +408,52 @@ export function extractFunctionParts(rootNode: JsNode): { }; } -export function transpileFn(rootNode: JsNode, minify: boolean): TranspilationResult { +export function transpileFn(rootNode: JsNode): TranspilationResult { const { params, body } = extractFunctionParts(rootNode); - const ctx: Context = new ContextImpl(minify, params); + const ctx: Context = { + externalNames: new Set(), + ignoreExternalDepth: 0, + visitedNodes: new Set(), + stack: [ + { + declaredNames: params.flatMap((param) => + param.type === FuncParameterType.identifier + ? param.name + : param.props.map((prop) => prop.alias), + ), + }, + ], + }; const tinyestBody = transpile(ctx, body); if (body.type === 'BlockStatement') { return { - params: ctx.params, + params, body: tinyestBody as tinyest.Block, externalNames: ctx.externalNames, }; } return { - params: ctx.params, + params, body: [NODE.block, [[NODE.return, tinyestBody as tinyest.Expression]]], externalNames: ctx.externalNames, }; } -// TODO: make this parameter mandatory -export function transpileNode(node: JsNode, minify = false): tinyest.AnyNode { - const ctx: Context = new ContextImpl(minify); +export function transpileNode(node: JsNode): tinyest.AnyNode { + const ctx: Context = { + externalNames: new Set(), + ignoreExternalDepth: 0, + visitedNodes: new Set(), + stack: [ + { + declaredNames: [], + }, + ], + }; return transpile(ctx, node); } - -class ContextImpl implements Context { - readonly externalNames: Map = new Map(); - ignoreExternalDepth = 0; - ignoreMinificationDepth = 0; - readonly visitedNodes: Set = new Set(); - readonly minifier: Minifier; - readonly stack: Scope[] = []; - readonly params: tinyest.FuncParameter[]; - - constructor(minify: boolean, params?: tinyest.FuncParameter[]) { - this.minifier = minify ? new MinifierImpl() : new MinifierNullImpl(); - this.params = (params ?? []).map((param) => { - if (param.type === FuncParameterType.identifier) { - return { ...param, name: this.minifier.minify(param.name) }; - } - return { - ...param, - props: param.props.map((prop) => ({ ...prop, alias: this.minifier.minify(prop.alias) })), - }; - }); - - const declaredNames = this.params.flatMap((param) => - param.type === FuncParameterType.identifier - ? param.name - : param.props.map((prop) => prop.alias), - ); - this.stack.push({ declaredNames }); - } -} diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index 08e4bfa794..f5a5f5ed4c 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -7,27 +7,13 @@ export type Scope = { declaredNames: string[]; }; -export type Externals = Map; - -export interface Minifier { - /** - * If `name` wasn't minified before, it gives it a new minified name. - * Then, returns the minified version of `name`. - */ - minify(name: string): string; - /** - * Returns the minified version of `name` if it exists, otherwise returns undefined. - */ - getIfMinified(name: string): string | undefined; -} +export type Externals = Set; export type Context = { /** Holds a set of all identifiers that were used in code, but were not declared in code. */ externalNames: Externals; /** Used to signal to identifiers that they should not treat their resolution as possible external uses. */ ignoreExternalDepth: number; - /** Used to signal to identifiers that they should not be minified (they are minified by default). */ - ignoreMinificationDepth: number; /** * Keeps the set of nodes visited by `tryFindExternalChain`. * This helps optimize code like `ext().x.y.z.t`: @@ -36,11 +22,6 @@ export type Context = { */ visitedNodes: Set; stack: Scope[]; - params: tinyest.FuncParameter[]; - /** - * Used to transform identifiers. - */ - minifier: Minifier; }; export type TranspilationResult = { diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index 6dac63e4c1..551ae468bb 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -86,7 +86,7 @@ export interface TransformMethods { this: PluginState, path: NodePath, name: string | undefined, - ast: ReturnType, + ast: UnpluginTranspilationResult, ): void; wrapInAutoName(this: PluginState, path: NodePath, name: string): void; @@ -455,9 +455,13 @@ function containsUseGpuDirective( .includes('use gpu'); } +type UnpluginTranspilationResult = Omit, 'externalNames'> & { + externalNames: Map; +}; + const fnNodeToTranspiledMap = new WeakMap< t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression, - ReturnType + UnpluginTranspilationResult >(); function functionOnExit( @@ -484,12 +488,15 @@ function functionOnExit( function transpile( rootNode: Parameters[0], obf: boolean, -): ReturnType { - const result = transpileFn(rootNode, false); +): UnpluginTranspilationResult { + const result = transpileFn(rootNode); if (obf) { return obfuscate(result); } - return result; + return { + ...result, + externalNames: new Map([...result.externalNames].map((external) => [external, external])), + }; } export const functionVisitor: TraverseOptions = { From 623e3f18fd790e450d7712d2dfb4f8fe138cead2 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:45:25 +0200 Subject: [PATCH 18/29] Make externalNames a map again --- packages/tinyest-for-wgsl/src/parsers.ts | 6 +++--- packages/tinyest-for-wgsl/src/types.ts | 2 +- packages/unplugin-typegpu/src/core/common.ts | 7 ++----- packages/unplugin-typegpu/src/core/obfuscate.ts | 6 +++--- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/tinyest-for-wgsl/src/parsers.ts b/packages/tinyest-for-wgsl/src/parsers.ts index 69299ac36d..9e17fef3c4 100644 --- a/packages/tinyest-for-wgsl/src/parsers.ts +++ b/packages/tinyest-for-wgsl/src/parsers.ts @@ -301,7 +301,7 @@ function transpile(ctx: Context, node: JsNode): tinyest.AnyNode { // add it to externals and swap the AST node for an identifier. const externalChain = tryFindExternalChain(ctx, node); if (externalChain) { - ctx.externalNames.add(externalChain); + ctx.externalNames.set(externalChain, externalChain); return externalChain; } } @@ -412,7 +412,7 @@ export function transpileFn(rootNode: JsNode): TranspilationResult { const { params, body } = extractFunctionParts(rootNode); const ctx: Context = { - externalNames: new Set(), + externalNames: new Map(), ignoreExternalDepth: 0, visitedNodes: new Set(), stack: [ @@ -445,7 +445,7 @@ export function transpileFn(rootNode: JsNode): TranspilationResult { export function transpileNode(node: JsNode): tinyest.AnyNode { const ctx: Context = { - externalNames: new Set(), + externalNames: new Map(), ignoreExternalDepth: 0, visitedNodes: new Set(), stack: [ diff --git a/packages/tinyest-for-wgsl/src/types.ts b/packages/tinyest-for-wgsl/src/types.ts index f5a5f5ed4c..5f27600786 100644 --- a/packages/tinyest-for-wgsl/src/types.ts +++ b/packages/tinyest-for-wgsl/src/types.ts @@ -7,7 +7,7 @@ export type Scope = { declaredNames: string[]; }; -export type Externals = Set; +export type Externals = Map; export type Context = { /** Holds a set of all identifiers that were used in code, but were not declared in code. */ diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index 551ae468bb..116f6651eb 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -455,7 +455,7 @@ function containsUseGpuDirective( .includes('use gpu'); } -type UnpluginTranspilationResult = Omit, 'externalNames'> & { +export type UnpluginTranspilationResult = Omit, 'externalNames'> & { externalNames: Map; }; @@ -493,10 +493,7 @@ function transpile( if (obf) { return obfuscate(result); } - return { - ...result, - externalNames: new Map([...result.externalNames].map((external) => [external, external])), - }; + return result; } export const functionVisitor: TraverseOptions = { diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index e50d93743f..ca467c7fb8 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -1,5 +1,5 @@ -import type { TranspilationResult } from '../../../tinyest-for-wgsl/src/types.ts'; -import { MinifierImpl, MinifierNullImpl, type Minifier } from './minifier.ts'; +import type { transpileFn } from 'tinyest-for-wgsl'; +import { MinifierImpl, type Minifier } from './minifier.ts'; import * as tinyest from 'tinyest'; const { NodeTypeCatalog: NODE } = tinyest; @@ -11,7 +11,7 @@ class Context { } } -export function obfuscate(fn: TranspilationResult) { +export function obfuscate(fn: ReturnType): ReturnType { const ctx = new Context(); const params = fn.params.map((param) => { From 0c874fefaf6269f85c20adb211ee8f9bde3df55f Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:46:14 +0200 Subject: [PATCH 19/29] Fix types --- .../tinyest-for-wgsl/tests/parsers.test.ts | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/packages/tinyest-for-wgsl/tests/parsers.test.ts b/packages/tinyest-for-wgsl/tests/parsers.test.ts index 601338f4a6..7ef040a636 100644 --- a/packages/tinyest-for-wgsl/tests/parsers.test.ts +++ b/packages/tinyest-for-wgsl/tests/parsers.test.ts @@ -14,7 +14,6 @@ describe('transpileFn', () => { const b = Infinity; const c = NaN; }`), - false, ); expect(params).toStrictEqual([]); @@ -35,14 +34,14 @@ describe('transpileFn', () => { it( 'fails when the input is not a function', dualTest((p) => { - expect(() => transpileFn(p('1 + 2'), false)).toThrow(); + expect(() => transpileFn(p('1 + 2'))).toThrow(); }), ); it( 'parses an empty arrow function', dualTest((p) => { - const { params, body, externalNames } = transpileFn(p('() => {}'), false); + const { params, body, externalNames } = transpileFn(p('() => {}')); expect(params).toStrictEqual([]); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[]]"`); @@ -53,7 +52,7 @@ describe('transpileFn', () => { it( 'parses an empty named function', dualTest((p) => { - const { params, body, externalNames } = transpileFn(p('function example() {}'), false); + const { params, body, externalNames } = transpileFn(p('function example() {}')); expect(params).toStrictEqual([]); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[]]"`); @@ -64,7 +63,7 @@ describe('transpileFn', () => { it( 'gathers external names', dualTest((p) => { - const { params, body, externalNames } = transpileFn(p('(a, b) => a + b - c'), false); + const { params, body, externalNames } = transpileFn(p('(a, b) => a + b - c')); expect(params).toStrictEqual([ { type: 'i', name: 'a' }, @@ -89,7 +88,6 @@ describe('transpileFn', () => { const a = 0; c = a + 2; }`), - false, ); expect(params).toStrictEqual([]); @@ -115,7 +113,6 @@ describe('transpileFn', () => { c = a + 2; } }`), - false, ); expect(params).toStrictEqual([]); @@ -134,7 +131,7 @@ describe('transpileFn', () => { it( 'treats the object as a possible external value when accessing a member', dualTest((p) => { - const { params, body, externalNames } = transpileFn(p('() => external.outside.prop'), false); + const { params, body, externalNames } = transpileFn(p('() => external.outside.prop')); expect(params).toStrictEqual([]); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,"external.outside.prop"]]]"`); @@ -154,7 +151,6 @@ describe('transpileFn', () => { p(`({ pos, a: b }) => { const x = pos.x; }`), - false, ); expect(params).toStrictEqual([ @@ -184,7 +180,6 @@ describe('transpileFn', () => { p(`(y, { pos, a: b }, {c, d}) => { const x = pos.x; }`), - false, ); expect(params).toStrictEqual([ @@ -225,7 +220,7 @@ describe('transpileFn', () => { ); it('handles TSNonNullExpression', () => { - const { body } = transpileFn(parseBabel('() => x!.y'), false); + const { body } = transpileFn(parseBabel('() => x!.y')); expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[7,"x","y"]]]]"`); }); @@ -242,7 +237,6 @@ describe('transpileFn', () => { value += a; // refers to an external 'a' return value; }`), - false, ); expect(externalNames).toMatchInlineSnapshot(` @@ -265,7 +259,6 @@ describe('transpileFn', () => { value += a; // refers to an external 'a' return value; }`), - false, ); expect(externalNames).toMatchInlineSnapshot(` @@ -300,7 +293,6 @@ describe('transpileFn', () => { const l = ext; }`), - false, ); expect(externalNames).toMatchInlineSnapshot(` @@ -334,7 +326,6 @@ describe('transpileFn', () => { const a = ext; const b = ext; }`), - false, ); expect(externalNames).toMatchInlineSnapshot(` @@ -355,7 +346,6 @@ describe('transpileFn', () => { const c = ext.config.zero; const d = ext.config.multiplier; };`), - false, ); expect(externalNames).toMatchInlineSnapshot(` @@ -391,7 +381,7 @@ describe('transpileFn', () => { const lastProp = props.at(-1) as ClassProperty | acorn.PropertyDefinition; const fn = lastProp.value as Expression | acorn.Expression; - const { externalNames } = transpileFn(fn, false); + const { externalNames } = transpileFn(fn); expect(externalNames).toMatchInlineSnapshot(` Map { From 6c5db9d863912d6665a517daad6e2465216eefc9 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:54:48 +0200 Subject: [PATCH 20/29] Use stringifyNode in obfuscation tests --- packages/typegpu/src/internal.ts | 1 + .../unplugin-typegpu/test/obfuscation.test.ts | 445 ++++++++++++++++++ 2 files changed, 446 insertions(+) create mode 100644 packages/unplugin-typegpu/test/obfuscation.test.ts diff --git a/packages/typegpu/src/internal.ts b/packages/typegpu/src/internal.ts index 348233398a..aa7b17936a 100644 --- a/packages/typegpu/src/internal.ts +++ b/packages/typegpu/src/internal.ts @@ -5,6 +5,7 @@ export { UnknownData } from './data/dataTypes.ts'; export { getName } from './shared/meta.ts'; export { WgslGenerator } from './tgsl/wgslGenerator.ts'; export { snip } from './data/snippet.ts'; +export { stringifyNode } from './shared/tseynit.ts'; // types export type { ResolutionCtx, FunctionArgument, TgpuShaderStage } from './types.ts'; diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts new file mode 100644 index 0000000000..6833755fe8 --- /dev/null +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -0,0 +1,445 @@ +import { + assertTSNamespaceExportDeclaration, + type ArrowFunctionExpression, + type FunctionDeclaration, +} from '@babel/types'; +import { transpileFn } from 'tinyest-for-wgsl'; +import { describe, expect, it } from 'vitest'; +import { obfuscate } from '../src/core/obfuscate.ts'; +import babelParser from '@babel/parser'; +import { stringifyNode } from 'typegpu/~internal'; + +// We only test tinyest -> tinyest transformation. +// We could write tinyest by hand, but this is more readable. +function parse(code: string): ArrowFunctionExpression { + const parsed = babelParser.parse(code, { sourceType: 'module', plugins: ['typescript'] }); + const maybeExpressionStatement = parsed.program.body[0]; + if (maybeExpressionStatement?.type !== 'ExpressionStatement') { + throw new Error( + `Invalid parse usage. Expected an expression statement (got ${maybeExpressionStatement?.type}).`, + ); + } + const maybeFunction = maybeExpressionStatement.expression; + if (maybeFunction?.type !== 'ArrowFunctionExpression') { + throw new Error( + `Invalid parse usage. Expected an arrow function expression (got ${maybeFunction?.type}).`, + ); + } + return maybeFunction; +} + +describe('transpileFn', () => { + it('minifies used variables', () => { + const code = `() => { const variable = 1; const other = 2; const sensitiveName = 3; }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + const a = 1; + const b = 2; + const c = 3; + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + // it( + // 'remembers minified names', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p('() => { const variable = 1; return variable; }'), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(`[]`); + // expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[13,"a",[5,"1"]],[10,"a"]]]"`); + // expect(externalNames).toMatchInlineSnapshot(`Map {}`); + // }), + // ); + + // it( + // 'remembers minified names in computed access', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p('() => { const variable = 1; const array = [1, 2]; return array[variable]; }'), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(`[]`); + // expect(JSON.stringify(body)).toMatchInlineSnapshot( + // `"[0,[[13,"a",[5,"1"]],[13,"b",[100,[[5,"1"],[5,"2"]]]],[10,[8,"b","a"]]]]"`, + // ); + // expect(externalNames).toMatchInlineSnapshot(`Map {}`); + // }), + // ); + + // it( + // 'remembers minified names in for loops', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p('() => { for (let i = 0; i< 10; i++) { return i; } }'), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(`[]`); + // expect(JSON.stringify(body)).toMatchInlineSnapshot( + // `"[0,[[14,[12,"a",[5,"0"]],[1,"a","<",[5,"10"]],[102,"++","a"],[0,[[10,"a"]]]]]]"`, + // ); + // expect(externalNames).toMatchInlineSnapshot(`Map {}`); + // }), + // ); + + // it( + // 'handles weird identifiers', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p(`() => { + // const a = undefined; + // const b = Infinity; + // const c = NaN; + // }`), + // true, + // ); + + // expect(params).toStrictEqual([]); + // expect(JSON.stringify(body)).toMatchInlineSnapshot( + // `"[0,[[13,"a","b"],[13,"c","d"],[13,"e","f"]]]"`, + // ); + // // These are identifiers, so they should be in externals. + // expect(externalNames).toMatchInlineSnapshot(` + // Map { + // "b" => "undefined", + // "d" => "Infinity", + // "f" => "NaN", + // } + // `); + // }), + // ); + + // it( + // 'minifies parameters', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p('(param1, param2) => { return param2 + param1; }'), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(` + // [ + // { + // "name": "a", + // "type": "i", + // }, + // { + // "name": "b", + // "type": "i", + // }, + // ] + // `); + // expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"b","+","a"]]]]"`); + // expect(externalNames).toMatchInlineSnapshot(`Map {}`); + // }), + // ); + + // it( + // 'minifies destructured parameters', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p('(param, { prop }) => { return param + prop; }'), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(` + // [ + // { + // "name": "a", + // "type": "i", + // }, + // { + // "props": [ + // { + // "alias": "b", + // "name": "prop", + // }, + // ], + // "type": "d", + // }, + // ] + // `); + // expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"a","+","b"]]]]"`); + // expect(externalNames).toMatchInlineSnapshot(`Map {}`); + // }), + // ); + + // it( + // 'minifies destructured parameters with aliases', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p('(param, { prop, other: alias }) => { return param + prop + alias; }'), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(` + // [ + // { + // "name": "a", + // "type": "i", + // }, + // { + // "props": [ + // { + // "alias": "b", + // "name": "prop", + // }, + // { + // "alias": "c", + // "name": "other", + // }, + // ], + // "type": "d", + // }, + // ] + // `); + // expect(JSON.stringify(body)).toMatchInlineSnapshot( + // `"[0,[[10,[1,[1,"a","+","b"],"+","c"]]]]"`, + // ); + // expect(externalNames).toMatchInlineSnapshot(`Map {}`); + // }), + // ); + + // it( + // 'does not minify struct props', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p('(param) => { let struct; return param.prop + struct.field; }'), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(` + // [ + // { + // "name": "a", + // "type": "i", + // }, + // ] + // `); + // expect(JSON.stringify(body)).toMatchInlineSnapshot( + // `"[0,[[12,"b"],[10,[1,[7,"a","prop"],"+",[7,"b","field"]]]]]"`, + // ); + // expect(externalNames).toMatchInlineSnapshot(`Map {}`); + // }), + // ); + + // it( + // 'does not minify struct keys', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p('(param) => { let struct = { field: 1 }; return struct.field; }'), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(` + // [ + // { + // "name": "a", + // "type": "i", + // }, + // ] + // `); + // expect(JSON.stringify(body)).toMatchInlineSnapshot( + // `"[0,[[12,"b",[104,{"field":[5,"1"]}]],[10,[7,"b","field"]]]]"`, + // ); + // expect(externalNames).toMatchInlineSnapshot(`Map {}`); + // }), + // ); + + // it( + // "minifies 'this'", + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p(`() => { return this.prop1.prop2; }`), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(`[]`); + // expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,"a"]]]"`); + // expect(externalNames).toMatchInlineSnapshot(` + // Map { + // "a" => "this.prop1.prop2", + // } + // `); + // }), + // ); + + // it( + // 'minifies externals', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p(`() => { + // const var1 = ext.value; + // const var2 = ext.config.multiplier; + // const var3 = ext.config.zero; + // const var4 = ext.config.multiplier; + // }`), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(`[]`); + // expect(JSON.stringify(body)).toMatchInlineSnapshot( + // `"[0,[[13,"a","b"],[13,"c","d"],[13,"e","f"],[13,"g","d"]]]"`, + // ); + // expect(externalNames).toMatchInlineSnapshot(` + // Map { + // "b" => "ext.value", + // "d" => "ext.config.multiplier", + // "f" => "ext.config.zero", + // } + // `); + // }), + // ); + + // it( + // 'minifies complex externals', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p(`() => { + // const h = ext.t.fn().prop; + // const i = ext.t.comp['computed'].prop; + // const j = ext.t.$.prop; + // const k = (ext).prop; + // }`), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(`[]`); + // expect(JSON.stringify(body)).toMatchInlineSnapshot( + // `"[0,[[13,"a",[7,[6,"b",[]],"prop"]],[13,"c",[7,[8,"d",[103,"computed"]],"prop"]],[13,"e",[7,[7,"f","$"],"prop"]],[13,"g","h"]]]"`, + // ); + // expect(externalNames).toMatchInlineSnapshot(` + // Map { + // "b" => "ext.t.fn", + // "d" => "ext.t.comp", + // "f" => "ext.t", + // "h" => "ext.prop", + // } + // `); + // }), + // ); + + // it( + // 'correctly handles variable shadowing', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p(`() => { + // const variable = 1; + // { + // const variable = 2; + // if (false) { + // return variable; + // } + // } + // return variable; + // }`), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(`[]`); + // expect(JSON.stringify(body)).toMatchInlineSnapshot( + // `"[0,[[13,"a",[5,"1"]],[0,[[13,"a",[5,"2"]],[11,false,[0,[[10,"a"]]]]]],[10,"a"]]]"`, + // ); + // expect(externalNames).toMatchInlineSnapshot(`Map {}`); + // }), + // ); + + // it( + // 'correctly handles parameter shadowing', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p(`(parameter) => { + // { + // const parameter = 2; + // if (false) { + // return parameter; + // } + // } + // return parameter; + // }`), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(` + // [ + // { + // "name": "a", + // "type": "i", + // }, + // ] + // `); + // expect(JSON.stringify(body)).toMatchInlineSnapshot( + // `"[0,[[0,[[13,"a",[5,"2"]],[11,false,[0,[[10,"a"]]]]]],[10,"a"]]]"`, + // ); + // expect(externalNames).toMatchInlineSnapshot(`Map {}`); + // }), + // ); + + // it( + // 'correctly handles external shadowing', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p(`() => { + // const variable = external; + // { + // const external = 1; + // return external; + // } + // return external; + // }`), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(`[]`); + // expect(JSON.stringify(body)).toMatchInlineSnapshot( + // `"[0,[[13,"a","b"],[0,[[13,"b",[5,"1"]],[10,"b"]]],[10,"b"]]]"`, + // ); + // expect(externalNames).toMatchInlineSnapshot(` + // Map { + // "b" => "external", + // } + // `); + // }), + // ); + + // it( + // 'supports more than 26 names', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p(`() => { ${Array.from({ length: 100 }, (_, i) => `let v${i};`).join('\n')} }`), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(`[]`); + // const stringifiedBody = JSON.stringify(body); + // expect(stringifiedBody).toContain('z'); + // expect(stringifiedBody).toContain('aa'); + // expect(stringifiedBody).toContain('ab'); + // expect(externalNames).toMatchInlineSnapshot(`Map {}`); + // }), + // ); + + // it( + // 'omits reserved words', + // dualTest((p) => { + // const { params, body, externalNames } = transpileFn( + // p(`() => { ${Array.from({ length: 26 + 26 * 26 }, (_, i) => `let v${i};`).join('\n')} }`), + // true, + // ); + + // expect(params).toMatchInlineSnapshot(`[]`); + // const stringifiedBody = JSON.stringify(body); + // expect(stringifiedBody).not.toContain('if'); + // expect(stringifiedBody).toContain('aaa'); + // expect(externalNames).toMatchInlineSnapshot(`Map {}`); + // }), + // ); +}); From bbca2aeca9d34bcba4f1bba2f7990ddad8b74904 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:12:28 +0200 Subject: [PATCH 21/29] Add remaining obfuscation tests --- .../tests/minification.test.ts | 420 --------- .../unplugin-typegpu/test/obfuscation.test.ts | 800 +++++++++--------- 2 files changed, 402 insertions(+), 818 deletions(-) delete mode 100644 packages/tinyest-for-wgsl/tests/minification.test.ts diff --git a/packages/tinyest-for-wgsl/tests/minification.test.ts b/packages/tinyest-for-wgsl/tests/minification.test.ts deleted file mode 100644 index 7be7b5cc39..0000000000 --- a/packages/tinyest-for-wgsl/tests/minification.test.ts +++ /dev/null @@ -1,420 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { transpileFn } from '../src/parsers.ts'; -import { dualTest } from './helpers.ts'; - -describe('transpileFn', () => { - it( - 'minifies used variables', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p('() => { const variable = 1; const other = 2; const sensitiveName = 3; }'), - true, - ); - - expect(params).toMatchInlineSnapshot(`[]`); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"a",[5,"1"]],[13,"b",[5,"2"]],[13,"c",[5,"3"]]]]"`, - ); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); - - it( - 'remembers minified names', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p('() => { const variable = 1; return variable; }'), - true, - ); - - expect(params).toMatchInlineSnapshot(`[]`); - expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[13,"a",[5,"1"]],[10,"a"]]]"`); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); - - it( - 'remembers minified names in computed access', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p('() => { const variable = 1; const array = [1, 2]; return array[variable]; }'), - true, - ); - - expect(params).toMatchInlineSnapshot(`[]`); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"a",[5,"1"]],[13,"b",[100,[[5,"1"],[5,"2"]]]],[10,[8,"b","a"]]]]"`, - ); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); - - it( - 'remembers minified names in for loops', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p('() => { for (let i = 0; i< 10; i++) { return i; } }'), - true, - ); - - expect(params).toMatchInlineSnapshot(`[]`); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[14,[12,"a",[5,"0"]],[1,"a","<",[5,"10"]],[102,"++","a"],[0,[[10,"a"]]]]]]"`, - ); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); - - it( - 'handles weird identifiers', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p(`() => { - const a = undefined; - const b = Infinity; - const c = NaN; - }`), - true, - ); - - expect(params).toStrictEqual([]); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"a","b"],[13,"c","d"],[13,"e","f"]]]"`, - ); - // These are identifiers, so they should be in externals. - expect(externalNames).toMatchInlineSnapshot(` - Map { - "b" => "undefined", - "d" => "Infinity", - "f" => "NaN", - } - `); - }), - ); - - it( - 'minifies parameters', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p('(param1, param2) => { return param2 + param1; }'), - true, - ); - - expect(params).toMatchInlineSnapshot(` - [ - { - "name": "a", - "type": "i", - }, - { - "name": "b", - "type": "i", - }, - ] - `); - expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"b","+","a"]]]]"`); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); - - it( - 'minifies destructured parameters', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p('(param, { prop }) => { return param + prop; }'), - true, - ); - - expect(params).toMatchInlineSnapshot(` - [ - { - "name": "a", - "type": "i", - }, - { - "props": [ - { - "alias": "b", - "name": "prop", - }, - ], - "type": "d", - }, - ] - `); - expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"a","+","b"]]]]"`); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); - - it( - 'minifies destructured parameters with aliases', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p('(param, { prop, other: alias }) => { return param + prop + alias; }'), - true, - ); - - expect(params).toMatchInlineSnapshot(` - [ - { - "name": "a", - "type": "i", - }, - { - "props": [ - { - "alias": "b", - "name": "prop", - }, - { - "alias": "c", - "name": "other", - }, - ], - "type": "d", - }, - ] - `); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[10,[1,[1,"a","+","b"],"+","c"]]]]"`, - ); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); - - it( - 'does not minify struct props', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p('(param) => { let struct; return param.prop + struct.field; }'), - true, - ); - - expect(params).toMatchInlineSnapshot(` - [ - { - "name": "a", - "type": "i", - }, - ] - `); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[12,"b"],[10,[1,[7,"a","prop"],"+",[7,"b","field"]]]]]"`, - ); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); - - it( - 'does not minify struct keys', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p('(param) => { let struct = { field: 1 }; return struct.field; }'), - true, - ); - - expect(params).toMatchInlineSnapshot(` - [ - { - "name": "a", - "type": "i", - }, - ] - `); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[12,"b",[104,{"field":[5,"1"]}]],[10,[7,"b","field"]]]]"`, - ); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); - - it( - "minifies 'this'", - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p(`() => { return this.prop1.prop2; }`), - true, - ); - - expect(params).toMatchInlineSnapshot(`[]`); - expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,"a"]]]"`); - expect(externalNames).toMatchInlineSnapshot(` - Map { - "a" => "this.prop1.prop2", - } - `); - }), - ); - - it( - 'minifies externals', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p(`() => { - const var1 = ext.value; - const var2 = ext.config.multiplier; - const var3 = ext.config.zero; - const var4 = ext.config.multiplier; - }`), - true, - ); - - expect(params).toMatchInlineSnapshot(`[]`); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"a","b"],[13,"c","d"],[13,"e","f"],[13,"g","d"]]]"`, - ); - expect(externalNames).toMatchInlineSnapshot(` - Map { - "b" => "ext.value", - "d" => "ext.config.multiplier", - "f" => "ext.config.zero", - } - `); - }), - ); - - it( - 'minifies complex externals', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p(`() => { - const h = ext.t.fn().prop; - const i = ext.t.comp['computed'].prop; - const j = ext.t.$.prop; - const k = (ext).prop; - }`), - true, - ); - - expect(params).toMatchInlineSnapshot(`[]`); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"a",[7,[6,"b",[]],"prop"]],[13,"c",[7,[8,"d",[103,"computed"]],"prop"]],[13,"e",[7,[7,"f","$"],"prop"]],[13,"g","h"]]]"`, - ); - expect(externalNames).toMatchInlineSnapshot(` - Map { - "b" => "ext.t.fn", - "d" => "ext.t.comp", - "f" => "ext.t", - "h" => "ext.prop", - } - `); - }), - ); - - it( - 'correctly handles variable shadowing', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p(`() => { - const variable = 1; - { - const variable = 2; - if (false) { - return variable; - } - } - return variable; - }`), - true, - ); - - expect(params).toMatchInlineSnapshot(`[]`); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"a",[5,"1"]],[0,[[13,"a",[5,"2"]],[11,false,[0,[[10,"a"]]]]]],[10,"a"]]]"`, - ); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); - - it( - 'correctly handles parameter shadowing', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p(`(parameter) => { - { - const parameter = 2; - if (false) { - return parameter; - } - } - return parameter; - }`), - true, - ); - - expect(params).toMatchInlineSnapshot(` - [ - { - "name": "a", - "type": "i", - }, - ] - `); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[0,[[13,"a",[5,"2"]],[11,false,[0,[[10,"a"]]]]]],[10,"a"]]]"`, - ); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); - - it( - 'correctly handles external shadowing', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p(`() => { - const variable = external; - { - const external = 1; - return external; - } - return external; - }`), - true, - ); - - expect(params).toMatchInlineSnapshot(`[]`); - expect(JSON.stringify(body)).toMatchInlineSnapshot( - `"[0,[[13,"a","b"],[0,[[13,"b",[5,"1"]],[10,"b"]]],[10,"b"]]]"`, - ); - expect(externalNames).toMatchInlineSnapshot(` - Map { - "b" => "external", - } - `); - }), - ); - - it( - 'supports more than 26 names', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p(`() => { ${Array.from({ length: 100 }, (_, i) => `let v${i};`).join('\n')} }`), - true, - ); - - expect(params).toMatchInlineSnapshot(`[]`); - const stringifiedBody = JSON.stringify(body); - expect(stringifiedBody).toContain('z'); - expect(stringifiedBody).toContain('aa'); - expect(stringifiedBody).toContain('ab'); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); - - it( - 'omits reserved words', - dualTest((p) => { - const { params, body, externalNames } = transpileFn( - p(`() => { ${Array.from({ length: 26 + 26 * 26 }, (_, i) => `let v${i};`).join('\n')} }`), - true, - ); - - expect(params).toMatchInlineSnapshot(`[]`); - const stringifiedBody = JSON.stringify(body); - expect(stringifiedBody).not.toContain('if'); - expect(stringifiedBody).toContain('aaa'); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }), - ); -}); diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index 6833755fe8..edf56398f4 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -44,402 +44,406 @@ describe('transpileFn', () => { `); expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); - // it( - // 'remembers minified names', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p('() => { const variable = 1; return variable; }'), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(`[]`); - // expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[13,"a",[5,"1"]],[10,"a"]]]"`); - // expect(externalNames).toMatchInlineSnapshot(`Map {}`); - // }), - // ); - - // it( - // 'remembers minified names in computed access', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p('() => { const variable = 1; const array = [1, 2]; return array[variable]; }'), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(`[]`); - // expect(JSON.stringify(body)).toMatchInlineSnapshot( - // `"[0,[[13,"a",[5,"1"]],[13,"b",[100,[[5,"1"],[5,"2"]]]],[10,[8,"b","a"]]]]"`, - // ); - // expect(externalNames).toMatchInlineSnapshot(`Map {}`); - // }), - // ); - - // it( - // 'remembers minified names in for loops', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p('() => { for (let i = 0; i< 10; i++) { return i; } }'), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(`[]`); - // expect(JSON.stringify(body)).toMatchInlineSnapshot( - // `"[0,[[14,[12,"a",[5,"0"]],[1,"a","<",[5,"10"]],[102,"++","a"],[0,[[10,"a"]]]]]]"`, - // ); - // expect(externalNames).toMatchInlineSnapshot(`Map {}`); - // }), - // ); - - // it( - // 'handles weird identifiers', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p(`() => { - // const a = undefined; - // const b = Infinity; - // const c = NaN; - // }`), - // true, - // ); - - // expect(params).toStrictEqual([]); - // expect(JSON.stringify(body)).toMatchInlineSnapshot( - // `"[0,[[13,"a","b"],[13,"c","d"],[13,"e","f"]]]"`, - // ); - // // These are identifiers, so they should be in externals. - // expect(externalNames).toMatchInlineSnapshot(` - // Map { - // "b" => "undefined", - // "d" => "Infinity", - // "f" => "NaN", - // } - // `); - // }), - // ); - - // it( - // 'minifies parameters', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p('(param1, param2) => { return param2 + param1; }'), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(` - // [ - // { - // "name": "a", - // "type": "i", - // }, - // { - // "name": "b", - // "type": "i", - // }, - // ] - // `); - // expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"b","+","a"]]]]"`); - // expect(externalNames).toMatchInlineSnapshot(`Map {}`); - // }), - // ); - - // it( - // 'minifies destructured parameters', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p('(param, { prop }) => { return param + prop; }'), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(` - // [ - // { - // "name": "a", - // "type": "i", - // }, - // { - // "props": [ - // { - // "alias": "b", - // "name": "prop", - // }, - // ], - // "type": "d", - // }, - // ] - // `); - // expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,[1,"a","+","b"]]]]"`); - // expect(externalNames).toMatchInlineSnapshot(`Map {}`); - // }), - // ); - - // it( - // 'minifies destructured parameters with aliases', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p('(param, { prop, other: alias }) => { return param + prop + alias; }'), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(` - // [ - // { - // "name": "a", - // "type": "i", - // }, - // { - // "props": [ - // { - // "alias": "b", - // "name": "prop", - // }, - // { - // "alias": "c", - // "name": "other", - // }, - // ], - // "type": "d", - // }, - // ] - // `); - // expect(JSON.stringify(body)).toMatchInlineSnapshot( - // `"[0,[[10,[1,[1,"a","+","b"],"+","c"]]]]"`, - // ); - // expect(externalNames).toMatchInlineSnapshot(`Map {}`); - // }), - // ); - - // it( - // 'does not minify struct props', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p('(param) => { let struct; return param.prop + struct.field; }'), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(` - // [ - // { - // "name": "a", - // "type": "i", - // }, - // ] - // `); - // expect(JSON.stringify(body)).toMatchInlineSnapshot( - // `"[0,[[12,"b"],[10,[1,[7,"a","prop"],"+",[7,"b","field"]]]]]"`, - // ); - // expect(externalNames).toMatchInlineSnapshot(`Map {}`); - // }), - // ); - - // it( - // 'does not minify struct keys', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p('(param) => { let struct = { field: 1 }; return struct.field; }'), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(` - // [ - // { - // "name": "a", - // "type": "i", - // }, - // ] - // `); - // expect(JSON.stringify(body)).toMatchInlineSnapshot( - // `"[0,[[12,"b",[104,{"field":[5,"1"]}]],[10,[7,"b","field"]]]]"`, - // ); - // expect(externalNames).toMatchInlineSnapshot(`Map {}`); - // }), - // ); - - // it( - // "minifies 'this'", - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p(`() => { return this.prop1.prop2; }`), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(`[]`); - // expect(JSON.stringify(body)).toMatchInlineSnapshot(`"[0,[[10,"a"]]]"`); - // expect(externalNames).toMatchInlineSnapshot(` - // Map { - // "a" => "this.prop1.prop2", - // } - // `); - // }), - // ); - - // it( - // 'minifies externals', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p(`() => { - // const var1 = ext.value; - // const var2 = ext.config.multiplier; - // const var3 = ext.config.zero; - // const var4 = ext.config.multiplier; - // }`), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(`[]`); - // expect(JSON.stringify(body)).toMatchInlineSnapshot( - // `"[0,[[13,"a","b"],[13,"c","d"],[13,"e","f"],[13,"g","d"]]]"`, - // ); - // expect(externalNames).toMatchInlineSnapshot(` - // Map { - // "b" => "ext.value", - // "d" => "ext.config.multiplier", - // "f" => "ext.config.zero", - // } - // `); - // }), - // ); - - // it( - // 'minifies complex externals', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p(`() => { - // const h = ext.t.fn().prop; - // const i = ext.t.comp['computed'].prop; - // const j = ext.t.$.prop; - // const k = (ext).prop; - // }`), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(`[]`); - // expect(JSON.stringify(body)).toMatchInlineSnapshot( - // `"[0,[[13,"a",[7,[6,"b",[]],"prop"]],[13,"c",[7,[8,"d",[103,"computed"]],"prop"]],[13,"e",[7,[7,"f","$"],"prop"]],[13,"g","h"]]]"`, - // ); - // expect(externalNames).toMatchInlineSnapshot(` - // Map { - // "b" => "ext.t.fn", - // "d" => "ext.t.comp", - // "f" => "ext.t", - // "h" => "ext.prop", - // } - // `); - // }), - // ); - - // it( - // 'correctly handles variable shadowing', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p(`() => { - // const variable = 1; - // { - // const variable = 2; - // if (false) { - // return variable; - // } - // } - // return variable; - // }`), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(`[]`); - // expect(JSON.stringify(body)).toMatchInlineSnapshot( - // `"[0,[[13,"a",[5,"1"]],[0,[[13,"a",[5,"2"]],[11,false,[0,[[10,"a"]]]]]],[10,"a"]]]"`, - // ); - // expect(externalNames).toMatchInlineSnapshot(`Map {}`); - // }), - // ); - - // it( - // 'correctly handles parameter shadowing', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p(`(parameter) => { - // { - // const parameter = 2; - // if (false) { - // return parameter; - // } - // } - // return parameter; - // }`), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(` - // [ - // { - // "name": "a", - // "type": "i", - // }, - // ] - // `); - // expect(JSON.stringify(body)).toMatchInlineSnapshot( - // `"[0,[[0,[[13,"a",[5,"2"]],[11,false,[0,[[10,"a"]]]]]],[10,"a"]]]"`, - // ); - // expect(externalNames).toMatchInlineSnapshot(`Map {}`); - // }), - // ); - - // it( - // 'correctly handles external shadowing', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p(`() => { - // const variable = external; - // { - // const external = 1; - // return external; - // } - // return external; - // }`), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(`[]`); - // expect(JSON.stringify(body)).toMatchInlineSnapshot( - // `"[0,[[13,"a","b"],[0,[[13,"b",[5,"1"]],[10,"b"]]],[10,"b"]]]"`, - // ); - // expect(externalNames).toMatchInlineSnapshot(` - // Map { - // "b" => "external", - // } - // `); - // }), - // ); - - // it( - // 'supports more than 26 names', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p(`() => { ${Array.from({ length: 100 }, (_, i) => `let v${i};`).join('\n')} }`), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(`[]`); - // const stringifiedBody = JSON.stringify(body); - // expect(stringifiedBody).toContain('z'); - // expect(stringifiedBody).toContain('aa'); - // expect(stringifiedBody).toContain('ab'); - // expect(externalNames).toMatchInlineSnapshot(`Map {}`); - // }), - // ); - - // it( - // 'omits reserved words', - // dualTest((p) => { - // const { params, body, externalNames } = transpileFn( - // p(`() => { ${Array.from({ length: 26 + 26 * 26 }, (_, i) => `let v${i};`).join('\n')} }`), - // true, - // ); - - // expect(params).toMatchInlineSnapshot(`[]`); - // const stringifiedBody = JSON.stringify(body); - // expect(stringifiedBody).not.toContain('if'); - // expect(stringifiedBody).toContain('aaa'); - // expect(externalNames).toMatchInlineSnapshot(`Map {}`); - // }), - // ); + + it('remembers minified names', () => { + const code = `() => { const variable = 1; return variable; }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + const a = 1; + return a; + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + + it('remembers minified names in computed access', () => { + const code = `() => { const variable = 1; const array = [1, 2]; return array[variable]; }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + const a = 1; + const b = [1, 2]; + return b[a]; + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + + it('remembers minified names in for loops', () => { + const code = `() => { for (let i = 0; i< 10; i++) { return i; } }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + for (let a = 0; a < 10; a++) { + return a; + } + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + + it('handles weird identifiers', () => { + const code = `() => { + const a = undefined; + const b = Infinity; + const c = NaN; + }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toStrictEqual([]); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + const a = b; + const c = d; + const e = f; + }" + `); + // These are identifiers, so they should be in externals. + expect(externalNames).toMatchInlineSnapshot(` + Map { + "b" => "undefined", + "d" => "Infinity", + "f" => "NaN", + } + `); + }); + + it('minifies parameters', () => { + const code = `(param1, param2) => { return param2 + param1; }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "a", + "type": "i", + }, + { + "name": "b", + "type": "i", + }, + ] + `); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + return b + a; + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + + it('minifies destructured parameters', () => { + const code = `(param, { prop }) => { return param + prop; }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "a", + "type": "i", + }, + { + "props": [ + { + "alias": "b", + "name": "prop", + }, + ], + "type": "d", + }, + ] + `); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + return a + b; + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + + it('minifies destructured parameters with aliases', () => { + const code = `(param, { prop, other: alias }) => { return param + prop + alias; }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "a", + "type": "i", + }, + { + "props": [ + { + "alias": "b", + "name": "prop", + }, + { + "alias": "c", + "name": "other", + }, + ], + "type": "d", + }, + ] + `); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + return (a + b) + c; + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + + it('does not minify struct props', () => { + const code = `(param) => { let struct; return param.prop + struct.field; }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "a", + "type": "i", + }, + ] + `); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + let b; + return a.prop + b.field; + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + + it('does not minify struct keys', () => { + const code = `(param) => { let struct = { field: 1 }; return struct.field; }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "a", + "type": "i", + }, + ] + `); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + let b = { field: 1 }; + return b.field; + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + + it("minifies 'this'", () => { + const code = `() => { return this.prop1.prop2; }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + return a; + }" + `); + expect(externalNames).toMatchInlineSnapshot(` + Map { + "a" => "this.prop1.prop2", + } + `); + }); + + it('minifies externals', () => { + const code = `() => { + const var1 = ext.value; + const var2 = ext.config.multiplier; + const var3 = ext.config.zero; + const var4 = ext.config.multiplier; + }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + const a = b; + const c = d; + const e = f; + const g = d; + }" + `); + expect(externalNames).toMatchInlineSnapshot(` + Map { + "b" => "ext.value", + "d" => "ext.config.multiplier", + "f" => "ext.config.zero", + } + `); + }); + + it('minifies complex externals', () => { + const code = `() => { + const h = ext.t.fn().prop; + const i = ext.t.comp['computed'].prop; + const j = ext.t.$.prop; + const k = (ext).prop; + }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + const a = b().prop; + const c = d["computed"].prop; + const e = f.$.prop; + const g = h; + }" + `); + expect(externalNames).toMatchInlineSnapshot(` + Map { + "b" => "ext.t.fn", + "d" => "ext.t.comp", + "f" => "ext.t", + "h" => "ext.prop", + } + `); + }); + + it('correctly handles variable shadowing', () => { + const code = `() => { + const variable = 1; + { + const variable = 2; + if (false) { + return variable; + } + } + return variable; + }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + const a = 1; + { + const a = 2; + if (false) { + return a; + } + } + return a; + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + + it('correctly handles parameter shadowing', () => { + const code = `(parameter) => { + { + const parameter = 2; + if (false) { + return parameter; + } + } + return parameter; + }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(` + [ + { + "name": "a", + "type": "i", + }, + ] + `); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + { + const a = 2; + if (false) { + return a; + } + } + return a; + }" + `); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + + it('correctly handles external shadowing', () => { + const code = `() => { + const variable = external; + { + const external = 1; + return external; + } + return external; + }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(`[]`); + expect(stringifyNode(body)).toMatchInlineSnapshot(` + "{ + const a = b; + { + const b = 1; + return b; + } + return b; + }" + `); + expect(externalNames).toMatchInlineSnapshot(` + Map { + "b" => "external", + } + `); + }); + + it('supports more than 26 names', () => { + const code = `() => { ${Array.from({ length: 100 }, (_, i) => `let v${i};`).join('\n')} }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(`[]`); + const stringifiedBody = stringifyNode(body); + expect(stringifiedBody).toContain('z'); + expect(stringifiedBody).toContain('aa'); + expect(stringifiedBody).toContain('ab'); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); + + it('omits reserved words', () => { + const code = `() => { ${Array.from({ length: 26 + 26 * 26 }, (_, i) => `let v${i};`).join('\n')} }`; + const transpiled = transpileFn(parse(code)); + const { params, body, externalNames } = obfuscate(transpiled); + + expect(params).toMatchInlineSnapshot(`[]`); + const stringifiedBody = stringifyNode(body); + expect(stringifiedBody).not.toContain('if'); + expect(stringifiedBody).toContain('aaa'); + expect(externalNames).toMatchInlineSnapshot(`Map {}`); + }); }); From 0534ae896458a7fb35032a188c2f5ddcf224c00f Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:27:31 +0200 Subject: [PATCH 22/29] Remove unused minifier functionality --- packages/typegpu/tests/tgslFn.test.ts | 17 ++- .../unplugin-typegpu/src/core/minifier.ts | 122 ------------------ .../unplugin-typegpu/src/core/obfuscate.ts | 55 +++++++- 3 files changed, 69 insertions(+), 125 deletions(-) delete mode 100644 packages/unplugin-typegpu/src/core/minifier.ts diff --git a/packages/typegpu/tests/tgslFn.test.ts b/packages/typegpu/tests/tgslFn.test.ts index 2f71e3de9f..cc70250b96 100644 --- a/packages/typegpu/tests/tgslFn.test.ts +++ b/packages/typegpu/tests/tgslFn.test.ts @@ -1,7 +1,7 @@ import { attest } from '@ark/attest'; import { describe, expect } from 'vitest'; import { builtin } from 'typegpu/data'; -import { tgpu, d, type TgpuFn, type TgpuSlot } from 'typegpu'; +import { tgpu, d, type TgpuFn, type TgpuSlot, std } from 'typegpu'; import { it } from 'typegpu-testing-utility'; describe('TGSL tgpu.fn function', () => { @@ -1098,6 +1098,21 @@ describe('tgsl fn when using plugin', () => { `); }); + it('does not accidentally shadow std', () => { + const fn = () => { + 'use gpu'; + const sin = 1; + const a = std.sin(sin); + }; + + expect(tgpu.resolve([fn])).toMatchInlineSnapshot(` + "fn fn_1() { + const sin_1 = 1; + let a = sin(f32(sin_1)); + }" + `); + }); + it('throws a readable error when assigning to a value defined outside of scope', () => { let a = 0; const f = () => { diff --git a/packages/unplugin-typegpu/src/core/minifier.ts b/packages/unplugin-typegpu/src/core/minifier.ts deleted file mode 100644 index aaaf2e4164..0000000000 --- a/packages/unplugin-typegpu/src/core/minifier.ts +++ /dev/null @@ -1,122 +0,0 @@ -export interface Minifier { - /** - * If `name` wasn't minified before, it gives it a new minified name. - * Then, returns the minified version of `name`. - */ - minify(name: string): string; - /** - * Returns the minified version of `name` if it exists, otherwise returns undefined. - */ - getIfMinified(name: string): string | undefined; -} - -export class MinifierNullImpl implements Minifier { - minify(name: string): string { - return name; - } - getIfMinified(name: string) { - return name; - // TODO: reconsider, this may backfire - } -} - -/** - * Generates all strings consisting of lowercase letters of the given length. - */ -function* combinationGenerator(length: number): Generator { - if (length === 0) { - yield ''; - return; - } - - for (let i = 97 /* ASCII a */; i <= 122 /* ASCII z */; i++) { - for (const name of combinationGenerator(length - 1)) { - yield `${String.fromCharCode(i)}${name}`; - } - } -} - -/** - * Generates fresh minified names, avoids forbidden tokens. - */ -function* freshNameGenerator(): Generator { - for (let i = 1; i <= 4; i++) { - for (const name of combinationGenerator(i)) { - if (!bannedTokens.has(name)) { - yield name; - } - } - } - throw new Error('Too many variable names!'); -} - -export class MinifierImpl implements Minifier { - #nameMap: Map = new Map(); - #nameGenerator: Generator = freshNameGenerator(); - - #generateFreshName(): string { - return this.#nameGenerator.next().value; - } - - minify(name: string): string { - let minifiedName = this.#nameMap.get(name); - if (!minifiedName) { - minifiedName = this.#generateFreshName(); - this.#nameMap.set(name, minifiedName); - } - - return minifiedName; - } - - getIfMinified(name: string) { - return this.#nameMap.get(name); - } -} - -// TODO: docs -// TODO: function names -export const bannedTokens = new Set([ - 'case', - 'else', - 'fn', - 'for', - 'if', - 'let', - 'loop', - 'true', - 'var', - 'NULL', - 'Self', - 'as', - 'asm', - 'auto', - 'cast', - 'do', - 'enum', - 'from', - 'get', - 'goto', - 'impl', - 'lowp', - 'meta', - 'mod', - 'move', - 'mut', - 'new', - 'nil', - 'null', - 'of', - 'pass', - 'priv', - 'pub', - 'ref', - 'self', - 'set', - 'std', - 'this', - 'try', - 'type', - 'use', - 'wgsl', - 'with', -]); diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index ca467c7fb8..3ca743db2a 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -1,13 +1,64 @@ import type { transpileFn } from 'tinyest-for-wgsl'; -import { MinifierImpl, type Minifier } from './minifier.ts'; import * as tinyest from 'tinyest'; const { NodeTypeCatalog: NODE } = tinyest; +/** + * Generates all strings consisting of lowercase letters of the given length. + */ +function* fixedLengthNameGenerator(length: number): Generator { + if (length === 0) { + yield ''; + return; + } + + for (let i = 97 /* ASCII a */; i <= 122 /* ASCII z */; i++) { + for (const name of fixedLengthNameGenerator(length - 1)) { + yield `${String.fromCharCode(i)}${name}`; + } + } +} + +/** + * Generates all strings consisting of lowercase letters. + */ +function* nameGenerator(): Generator { + for (let i = 1; ; i++) { + for (const name of fixedLengthNameGenerator(i)) { + yield name; + } + } +} + +class Minifier { + #nameMap: Map = new Map(); + #nameGenerator: Generator = nameGenerator(); + + #generateFreshName(): string { + return this.#nameGenerator.next().value; + } + + /** + * If `name` wasn't minified before, it gives it a new minified name. + * Then, returns the minified version of `name`. + */ + minify(name: string): string { + let minifiedName = this.#nameMap.get(name); + if (!minifiedName) { + minifiedName = this.#generateFreshName(); + this.#nameMap.set(name, minifiedName); + } + + return minifiedName; + } +} + +// TODO: docs + class Context { minifier: Minifier; constructor() { - this.minifier = new MinifierImpl(); + this.minifier = new Minifier(); } } From f57d387c7482c4a5d56a6f54ec512004b760f2b5 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:54:52 +0200 Subject: [PATCH 23/29] Minify -> obfuscate --- packages/unplugin-typegpu/src/core/common.ts | 24 ++++++-------- .../unplugin-typegpu/src/core/obfuscate.ts | 32 +++++++++---------- .../test/minification.test.ts | 14 ++++---- .../unplugin-typegpu/test/obfuscation.test.ts | 24 +++++++------- 4 files changed, 45 insertions(+), 49 deletions(-) diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index 116f6651eb..dab6ff9f49 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -28,12 +28,12 @@ export interface Options { autoNamingEnabled?: boolean; /** - * Minify the generated AST. + * Obfuscate the generated AST. * This results in obfuscation of the generated WGSL, and in smaller bundle sizes. * * @default false */ - minify?: boolean; + obfuscate?: boolean; /** * Skipping files that don't contain "typegpu", "tgpu" or "use gpu". @@ -86,7 +86,7 @@ export interface TransformMethods { this: PluginState, path: NodePath, name: string | undefined, - ast: UnpluginTranspilationResult, + ast: ReturnType, ): void; wrapInAutoName(this: PluginState, path: NodePath, name: string): void; @@ -146,7 +146,7 @@ export const defaultOptions = { include: /\.m?[jt]sx?(?:\?.*)?$/, autoNamingEnabled: true, earlyPruning: true, - minify: false, + obfuscate: true, } satisfies Partial; /** @@ -455,13 +455,9 @@ function containsUseGpuDirective( .includes('use gpu'); } -export type UnpluginTranspilationResult = Omit, 'externalNames'> & { - externalNames: Map; -}; - const fnNodeToTranspiledMap = new WeakMap< t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression, - UnpluginTranspilationResult + ReturnType >(); function functionOnExit( @@ -488,7 +484,7 @@ function functionOnExit( function transpile( rootNode: Parameters[0], obf: boolean, -): UnpluginTranspilationResult { +): ReturnType { const result = transpileFn(rootNode); if (obf) { return obfuscate(result); @@ -554,7 +550,7 @@ export const functionVisitor: TraverseOptions = { ArrowFunctionExpression: { enter(path, state) { if (containsUseGpuDirective(path.node)) { - fnNodeToTranspiledMap.set(path.node, transpile(path.node, this.opts.minify)); + fnNodeToTranspiledMap.set(path.node, transpile(path.node, this.opts.obfuscate)); if (state.inUseGpuScope) { throw new Error(`Nesting 'use gpu' functions is not allowed`); } @@ -567,7 +563,7 @@ export const functionVisitor: TraverseOptions = { FunctionExpression: { enter(path, state) { if (containsUseGpuDirective(path.node)) { - fnNodeToTranspiledMap.set(path.node, transpile(path.node, this.opts.minify)); + fnNodeToTranspiledMap.set(path.node, transpile(path.node, this.opts.obfuscate)); if (state.inUseGpuScope) { throw new Error(`Nesting 'use gpu' functions is not allowed`); } @@ -580,7 +576,7 @@ export const functionVisitor: TraverseOptions = { FunctionDeclaration: { enter(path, state) { if (containsUseGpuDirective(path.node)) { - fnNodeToTranspiledMap.set(path.node, transpile(path.node, this.opts.minify)); + fnNodeToTranspiledMap.set(path.node, transpile(path.node, this.opts.obfuscate)); if (state.inUseGpuScope) { throw new Error(`Nesting 'use gpu' functions is not allowed`); } @@ -609,7 +605,7 @@ export const functionVisitor: TraverseOptions = { t.ArrowFunctionExpression | t.FunctionDeclaration | t.FunctionExpression >, getFunctionName(path.get('arguments.0')), - transpile(implementation, this.opts.minify), + transpile(implementation, this.opts.obfuscate), ); } } diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index 3ca743db2a..1b5d18a705 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -29,7 +29,7 @@ function* nameGenerator(): Generator { } } -class Minifier { +class Obfuscator { #nameMap: Map = new Map(); #nameGenerator: Generator = nameGenerator(); @@ -38,27 +38,27 @@ class Minifier { } /** - * If `name` wasn't minified before, it gives it a new minified name. - * Then, returns the minified version of `name`. + * If `name` wasn't obfuscated before, it gives it a new obfuscated name. + * Then, returns the obfuscated version of `name`. */ - minify(name: string): string { - let minifiedName = this.#nameMap.get(name); - if (!minifiedName) { - minifiedName = this.#generateFreshName(); - this.#nameMap.set(name, minifiedName); + obfuscate(name: string): string { + let obfuscatedName = this.#nameMap.get(name); + if (!obfuscatedName) { + obfuscatedName = this.#generateFreshName(); + this.#nameMap.set(name, obfuscatedName); } - return minifiedName; + return obfuscatedName; } } // TODO: docs class Context { - minifier: Minifier; + obfuscator: Obfuscator; constructor() { - this.minifier = new Minifier(); + this.obfuscator = new Obfuscator(); } } @@ -67,18 +67,18 @@ export function obfuscate(fn: ReturnType): ReturnType { if (param.type === 'i') { - return { ...param, name: ctx.minifier.minify(param.name) }; + return { ...param, name: ctx.obfuscator.obfuscate(param.name) }; } return { ...param, - props: param.props.map((prop) => ({ ...prop, alias: ctx.minifier.minify(prop.alias) })), + props: param.props.map((prop) => ({ ...prop, alias: ctx.obfuscator.obfuscate(prop.alias) })), }; }); const body = obf(ctx, fn.body); const externalNames = new Map(); - fn.externalNames.forEach((key, value) => externalNames.set(ctx.minifier.minify(key), value)); + fn.externalNames.forEach((key, value) => externalNames.set(ctx.obfuscator.obfuscate(key), value)); return { params, body, externalNames }; } @@ -194,8 +194,8 @@ function obf(ctx: Context, node: T): T { } if (typeof node === 'string') { - // If we got here, then this identifier should be minified. - return ctx.minifier.minify(node) as T; + // If we got here, then this identifier should be obfuscated. + return ctx.obfuscator.obfuscate(node) as T; } if (typeof node === 'boolean') { diff --git a/packages/unplugin-typegpu/test/minification.test.ts b/packages/unplugin-typegpu/test/minification.test.ts index 45755dbeb3..6a0f81d964 100644 --- a/packages/unplugin-typegpu/test/minification.test.ts +++ b/packages/unplugin-typegpu/test/minification.test.ts @@ -2,9 +2,9 @@ import { expect, test } from 'vitest'; import { babelTransform, rollupTransform } from './transform.ts'; import { describe } from 'node:test'; -// No need to test the minification in-depth, as it is already tested in tinyest-for-wgsl. -describe('minification', () => { - describe('assigns minified metadata', () => { +// No need to test the obfuscation in-depth, as it is already tested in obfuscation.test.ts. +describe('obfuscation', () => { + describe('assigns obfuscation metadata', () => { const code = `\ import { tgpu } from 'typegpu'; @@ -17,7 +17,7 @@ describe('minification', () => { };`; test('[BABEL]', () => { - expect(babelTransform(code, { minify: true })).toMatchInlineSnapshot(` + expect(babelTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` "import { tgpu } from 'typegpu'; const external = { n: 1 @@ -43,7 +43,7 @@ describe('minification', () => { }); test('[ROLLUP]', async () => { - expect(await rollupTransform(code, { minify: true })).toMatchInlineSnapshot(` + expect(await rollupTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` "import 'typegpu'; const external = { n: 1 }; @@ -77,7 +77,7 @@ describe('minification', () => { }`; test('[BABEL]', () => { - expect(babelTransform(code, { minify: true })).toMatchInlineSnapshot(` + expect(babelTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` "import { tgpu } from 'typegpu'; export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = () => { const a = undefined; @@ -100,7 +100,7 @@ describe('minification', () => { }); test('[ROLLUP]', async () => { - expect(await rollupTransform(code, { minify: true })).toMatchInlineSnapshot(` + expect(await rollupTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` "import 'typegpu'; const fn = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = (() => { diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index edf56398f4..8d544ad517 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -29,7 +29,7 @@ function parse(code: string): ArrowFunctionExpression { } describe('transpileFn', () => { - it('minifies used variables', () => { + it('obfuscates used variables', () => { const code = `() => { const variable = 1; const other = 2; const sensitiveName = 3; }`; const transpiled = transpileFn(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -45,7 +45,7 @@ describe('transpileFn', () => { expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); - it('remembers minified names', () => { + it('remembers obfuscated names', () => { const code = `() => { const variable = 1; return variable; }`; const transpiled = transpileFn(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -60,7 +60,7 @@ describe('transpileFn', () => { expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); - it('remembers minified names in computed access', () => { + it('remembers obfuscated names in computed access', () => { const code = `() => { const variable = 1; const array = [1, 2]; return array[variable]; }`; const transpiled = transpileFn(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -76,7 +76,7 @@ describe('transpileFn', () => { expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); - it('remembers minified names in for loops', () => { + it('remembers obfuscated names in for loops', () => { const code = `() => { for (let i = 0; i< 10; i++) { return i; } }`; const transpiled = transpileFn(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -119,7 +119,7 @@ describe('transpileFn', () => { `); }); - it('minifies parameters', () => { + it('obfuscates parameters', () => { const code = `(param1, param2) => { return param2 + param1; }`; const transpiled = transpileFn(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -144,7 +144,7 @@ describe('transpileFn', () => { expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); - it('minifies destructured parameters', () => { + it('obfuscates destructured parameters', () => { const code = `(param, { prop }) => { return param + prop; }`; const transpiled = transpileFn(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -174,7 +174,7 @@ describe('transpileFn', () => { expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); - it('minifies destructured parameters with aliases', () => { + it('obfuscates destructured parameters with aliases', () => { const code = `(param, { prop, other: alias }) => { return param + prop + alias; }`; const transpiled = transpileFn(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -208,7 +208,7 @@ describe('transpileFn', () => { expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); - it('does not minify struct props', () => { + it('does not obfuscate struct props', () => { const code = `(param) => { let struct; return param.prop + struct.field; }`; const transpiled = transpileFn(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -230,7 +230,7 @@ describe('transpileFn', () => { expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); - it('does not minify struct keys', () => { + it('does not obfuscate struct keys', () => { const code = `(param) => { let struct = { field: 1 }; return struct.field; }`; const transpiled = transpileFn(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -252,7 +252,7 @@ describe('transpileFn', () => { expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); - it("minifies 'this'", () => { + it("obfuscates 'this'", () => { const code = `() => { return this.prop1.prop2; }`; const transpiled = transpileFn(parse(code)); const { params, body, externalNames } = obfuscate(transpiled); @@ -270,7 +270,7 @@ describe('transpileFn', () => { `); }); - it('minifies externals', () => { + it('obfuscates externals', () => { const code = `() => { const var1 = ext.value; const var2 = ext.config.multiplier; @@ -298,7 +298,7 @@ describe('transpileFn', () => { `); }); - it('minifies complex externals', () => { + it('obfuscates complex externals', () => { const code = `() => { const h = ext.t.fn().prop; const i = ext.t.comp['computed'].prop; From 837884e80eb3de3523fc32d7cfff3887a06a37a1 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 4 Aug 2026 11:20:24 +0200 Subject: [PATCH 24/29] Merge test files --- .../unplugin-typegpu/src/core/obfuscate.ts | 6 +- .../test/minification.test.ts | 119 ----------------- .../unplugin-typegpu/test/obfuscation.test.ts | 126 +++++++++++++++++- 3 files changed, 122 insertions(+), 129 deletions(-) delete mode 100644 packages/unplugin-typegpu/test/minification.test.ts diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index 1b5d18a705..b0f1b57f45 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -186,7 +186,7 @@ const nodeIdToName = new Map(Object.entries(NODE).map(([key, value]) => [value, /** * Traverses the AST and generates a new one that is obfuscated. * Copies old AST when identifiers cannot appear in a subtree, - * e.g. in a member access property, or for operator ('=', '<', ...) nodes. + * e.g. in a member access property, or for operator nodes ('=', '<', ...). */ function obf(ctx: Context, node: T): T { if (node === null) { @@ -204,11 +204,11 @@ function obf(ctx: Context, node: T): T { const nodeName: keyof typeof visitors | undefined = nodeIdToName.get(node[0]); if (nodeName === undefined) { - throw new Error('AAA'); + throw new Error(`Internal error, no name for node type ${node[0]}.`); } const visitor = visitors[nodeName] as unknown as ((ctx: Context, node: T) => T) | undefined; if (!visitor) { - throw new Error('BBB'); + throw new Error(`Internal error, no visitor for node '${nodeName}'.`); } return visitor(ctx, node); } diff --git a/packages/unplugin-typegpu/test/minification.test.ts b/packages/unplugin-typegpu/test/minification.test.ts deleted file mode 100644 index 6a0f81d964..0000000000 --- a/packages/unplugin-typegpu/test/minification.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { expect, test } from 'vitest'; -import { babelTransform, rollupTransform } from './transform.ts'; -import { describe } from 'node:test'; - -// No need to test the obfuscation in-depth, as it is already tested in obfuscation.test.ts. -describe('obfuscation', () => { - describe('assigns obfuscation metadata', () => { - const code = `\ - import { tgpu } from 'typegpu'; - - const external = { n: 1 } - - export const fn = (argument) => { - 'use gpu'; - const variable = 3; - return external.n + argument + variable; - };`; - - test('[BABEL]', () => { - expect(babelTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` - "import { tgpu } from 'typegpu'; - const external = { - n: 1 - }; - export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = argument => { - const variable = 3; - return __tsover_add(__tsover_add(external.n, argument), variable); - }, { - v: 2, - name: "fn", - ast: { - params: [{ - type: "i", - name: "a" - }], - body: [0, [[13, "b", [5, "3"]], [10, [1, [1, "c", "+", "a"], "+", "b"]]]] - }, - externals: { - "c": () => external.n - } - }) && $.f)({});" - `); - }); - - test('[ROLLUP]', async () => { - expect(await rollupTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` - "import 'typegpu'; - - const external = { n: 1 }; - - const fn = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = ((argument) => { - - const variable = 3; - return __tsover_add(__tsover_add(external.n, argument), variable); - }), { - v: 2, - name: "fn", - ast: {"params":[{"type":"i","name":"a"}],"body":[0,[[13,"b",[5,"3"]],[10,[1,[1,"c","+","a"],"+","b"]]]]}, - externals: {"c":() => external.n} - }) && $.f)({})); - - export { fn }; - " - `); - }); - }); - - describe('weird identifiers', () => { - const code = ` - import { tgpu } from 'typegpu'; - - export const fn = () => { - 'use gpu'; - const a = undefined; - const b = Infinity; - const c = NaN; - }`; - - test('[BABEL]', () => { - expect(babelTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` - "import { tgpu } from 'typegpu'; - export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = () => { - const a = undefined; - const b = Infinity; - const c = NaN; - }, { - v: 2, - name: "fn", - ast: { - params: [], - body: [0, [[13, "a", "b"], [13, "c", "d"], [13, "e", "f"]]] - }, - externals: { - "b": () => undefined, - "d": () => Infinity, - "f": () => NaN - } - }) && $.f)({});" - `); - }); - - test('[ROLLUP]', async () => { - expect(await rollupTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` - "import 'typegpu'; - - const fn = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = (() => { - }), { - v: 2, - name: "fn", - ast: {"params":[],"body":[0,[[13,"a","b"],[13,"c","d"],[13,"e","f"]]]}, - externals: {"b":() => undefined,"d":() => Infinity,"f":() => NaN} - }) && $.f)({})); - - export { fn }; - " - `); - }); - }); -}); diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index 8d544ad517..4139d343e5 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -1,13 +1,125 @@ -import { - assertTSNamespaceExportDeclaration, - type ArrowFunctionExpression, - type FunctionDeclaration, -} from '@babel/types'; +import { type ArrowFunctionExpression } from '@babel/types'; import { transpileFn } from 'tinyest-for-wgsl'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, test } from 'vitest'; import { obfuscate } from '../src/core/obfuscate.ts'; import babelParser from '@babel/parser'; import { stringifyNode } from 'typegpu/~internal'; +import { babelTransform, rollupTransform } from './transform.ts'; + +describe('plugin obfuscation', () => { + describe('assigns obfuscation metadata', () => { + const code = `\ + import { tgpu } from 'typegpu'; + + const external = { n: 1 } + + export const fn = (argument) => { + 'use gpu'; + const variable = 3; + return external.n + argument + variable; + };`; + + test('[BABEL]', () => { + expect(babelTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` + "import { tgpu } from 'typegpu'; + const external = { + n: 1 + }; + export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = argument => { + const variable = 3; + return __tsover_add(__tsover_add(external.n, argument), variable); + }, { + v: 2, + name: "fn", + ast: { + params: [{ + type: "i", + name: "a" + }], + body: [0, [[13, "b", [5, "3"]], [10, [1, [1, "c", "+", "a"], "+", "b"]]]] + }, + externals: { + "c": () => external.n + } + }) && $.f)({});" + `); + }); + + test('[ROLLUP]', async () => { + expect(await rollupTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` + "import 'typegpu'; + + const external = { n: 1 }; + + const fn = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = ((argument) => { + + const variable = 3; + return __tsover_add(__tsover_add(external.n, argument), variable); + }), { + v: 2, + name: "fn", + ast: {"params":[{"type":"i","name":"a"}],"body":[0,[[13,"b",[5,"3"]],[10,[1,[1,"c","+","a"],"+","b"]]]]}, + externals: {"c":() => external.n} + }) && $.f)({})); + + export { fn }; + " + `); + }); + }); + + describe('weird identifiers', () => { + const code = ` + import { tgpu } from 'typegpu'; + + export const fn = () => { + 'use gpu'; + const a = undefined; + const b = Infinity; + const c = NaN; + }`; + + test('[BABEL]', () => { + expect(babelTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` + "import { tgpu } from 'typegpu'; + export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = () => { + const a = undefined; + const b = Infinity; + const c = NaN; + }, { + v: 2, + name: "fn", + ast: { + params: [], + body: [0, [[13, "a", "b"], [13, "c", "d"], [13, "e", "f"]]] + }, + externals: { + "b": () => undefined, + "d": () => Infinity, + "f": () => NaN + } + }) && $.f)({});" + `); + }); + + test('[ROLLUP]', async () => { + expect(await rollupTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` + "import 'typegpu'; + + const fn = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = (() => { + }), { + v: 2, + name: "fn", + ast: {"params":[],"body":[0,[[13,"a","b"],[13,"c","d"],[13,"e","f"]]]}, + externals: {"b":() => undefined,"d":() => Infinity,"f":() => NaN} + }) && $.f)({})); + + export { fn }; + " + `); + }); + }); +}); // We only test tinyest -> tinyest transformation. // We could write tinyest by hand, but this is more readable. @@ -28,7 +140,7 @@ function parse(code: string): ArrowFunctionExpression { return maybeFunction; } -describe('transpileFn', () => { +describe('obfuscate', () => { it('obfuscates used variables', () => { const code = `() => { const variable = 1; const other = 2; const sensitiveName = 3; }`; const transpiled = transpileFn(parse(code)); From 4b5551d8061e5ca8e6451bea84e47c2ee8082c37 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:15:18 +0200 Subject: [PATCH 25/29] Rename obfuscate to EXPERIMENTAL_obfuscate --- packages/unplugin-typegpu/src/core/common.ts | 21 +++++++++++++------ .../unplugin-typegpu/src/core/obfuscate.ts | 2 -- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index dab6ff9f49..304ab76493 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -33,7 +33,7 @@ export interface Options { * * @default false */ - obfuscate?: boolean; + EXPERIMENTAL_obfuscate?: boolean; /** * Skipping files that don't contain "typegpu", "tgpu" or "use gpu". @@ -146,7 +146,7 @@ export const defaultOptions = { include: /\.m?[jt]sx?(?:\?.*)?$/, autoNamingEnabled: true, earlyPruning: true, - obfuscate: true, + EXPERIMENTAL_obfuscate: false, } satisfies Partial; /** @@ -550,7 +550,10 @@ export const functionVisitor: TraverseOptions = { ArrowFunctionExpression: { enter(path, state) { if (containsUseGpuDirective(path.node)) { - fnNodeToTranspiledMap.set(path.node, transpile(path.node, this.opts.obfuscate)); + fnNodeToTranspiledMap.set( + path.node, + transpile(path.node, this.opts.EXPERIMENTAL_obfuscate), + ); if (state.inUseGpuScope) { throw new Error(`Nesting 'use gpu' functions is not allowed`); } @@ -563,7 +566,10 @@ export const functionVisitor: TraverseOptions = { FunctionExpression: { enter(path, state) { if (containsUseGpuDirective(path.node)) { - fnNodeToTranspiledMap.set(path.node, transpile(path.node, this.opts.obfuscate)); + fnNodeToTranspiledMap.set( + path.node, + transpile(path.node, this.opts.EXPERIMENTAL_obfuscate), + ); if (state.inUseGpuScope) { throw new Error(`Nesting 'use gpu' functions is not allowed`); } @@ -576,7 +582,10 @@ export const functionVisitor: TraverseOptions = { FunctionDeclaration: { enter(path, state) { if (containsUseGpuDirective(path.node)) { - fnNodeToTranspiledMap.set(path.node, transpile(path.node, this.opts.obfuscate)); + fnNodeToTranspiledMap.set( + path.node, + transpile(path.node, this.opts.EXPERIMENTAL_obfuscate), + ); if (state.inUseGpuScope) { throw new Error(`Nesting 'use gpu' functions is not allowed`); } @@ -605,7 +614,7 @@ export const functionVisitor: TraverseOptions = { t.ArrowFunctionExpression | t.FunctionDeclaration | t.FunctionExpression >, getFunctionName(path.get('arguments.0')), - transpile(implementation, this.opts.obfuscate), + transpile(implementation, this.opts.EXPERIMENTAL_obfuscate), ); } } diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index b0f1b57f45..8b066c5d7f 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -52,8 +52,6 @@ class Obfuscator { } } -// TODO: docs - class Context { obfuscator: Obfuscator; From aa3c7c5dfef35c66cdf6dda2c75afae4fa24ebea Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:33:40 +0200 Subject: [PATCH 26/29] Add options check --- packages/unplugin-typegpu/src/babel.ts | 3 +- packages/unplugin-typegpu/src/bun.ts | 4 +-- packages/unplugin-typegpu/src/core/common.ts | 9 +++++ packages/unplugin-typegpu/src/core/factory.ts | 3 +- .../unplugin-typegpu/test/obfuscation.test.ts | 36 ++++++++++++++++--- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/packages/unplugin-typegpu/src/babel.ts b/packages/unplugin-typegpu/src/babel.ts index ceb66a99d0..eabfd52a60 100644 --- a/packages/unplugin-typegpu/src/babel.ts +++ b/packages/unplugin-typegpu/src/babel.ts @@ -6,6 +6,7 @@ import { METADATA_FORMAT_VERSION, type MetadatableFunction, type PluginState, + checkOpts, defaultOptions, functionVisitor, getBlockScope, @@ -169,7 +170,7 @@ export default function TypeGPUPlugin() { return { name: 'typegpu', pre(this: PluginState) { - this.opts = defu(this.opts, defaultOptions); + this.opts = checkOpts(defu(this.opts, defaultOptions)); initPluginState(this, { warn: (message) => console.warn(message), assignMetadata, diff --git a/packages/unplugin-typegpu/src/bun.ts b/packages/unplugin-typegpu/src/bun.ts index 1f2e8bb558..1bb4f864af 100644 --- a/packages/unplugin-typegpu/src/bun.ts +++ b/packages/unplugin-typegpu/src/bun.ts @@ -1,10 +1,10 @@ import defu from 'defu'; -import { defaultOptions, earlyPruneRegex, type Options } from './core/common.ts'; +import { checkOpts, defaultOptions, earlyPruneRegex, type Options } from './core/common.ts'; import { unpluginFactory } from './core/factory.ts'; import type { UnpluginBuildContext, UnpluginContext } from 'unplugin'; export default (rawOptions?: Options): Bun.BunPlugin => { - const options = defu(rawOptions, defaultOptions); + const options = checkOpts(defu(rawOptions, defaultOptions)); const include = options.include; if (!(include instanceof RegExp)) { throw new Error( diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index 304ab76493..19c03f8f89 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -45,6 +45,15 @@ export interface Options { earlyPruning?: boolean | undefined; } +export function checkOpts(opts: T): T { + if (opts.EXPERIMENTAL_obfuscate && opts.autoNamingEnabled) { + throw new Error( + `Options 'EXPERIMENTAL_obfuscate' and 'autoNamingEnabled' cannot be enabled at the same time.`, + ); + } + return opts; +} + export type MetadatableFunction = | t.FunctionDeclaration | t.FunctionExpression diff --git a/packages/unplugin-typegpu/src/core/factory.ts b/packages/unplugin-typegpu/src/core/factory.ts index c18b76397f..69e7ebb29d 100644 --- a/packages/unplugin-typegpu/src/core/factory.ts +++ b/packages/unplugin-typegpu/src/core/factory.ts @@ -13,6 +13,7 @@ import { functionVisitor, getBlockScope, METADATA_FORMAT_VERSION, + checkOpts, } from './common.ts'; import type { Options, UnpluginPluginState, MetadatableFunction, NodeLocation } from './common.ts'; @@ -151,7 +152,7 @@ const NodeUtils = { }; export const unpluginFactory = ((rawOptions, _meta) => { - const options = defu(rawOptions, defaultOptions); + const options = checkOpts(defu(rawOptions, defaultOptions)); return { name: 'unplugin-typegpu' as const, diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index 4139d343e5..f967bcc259 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -5,6 +5,8 @@ import { obfuscate } from '../src/core/obfuscate.ts'; import babelParser from '@babel/parser'; import { stringifyNode } from 'typegpu/~internal'; import { babelTransform, rollupTransform } from './transform.ts'; +import { bunPlugin, rollupPlugin } from '../src/index.ts'; +import { defaultOptions } from '../src/core/common.ts'; describe('plugin obfuscation', () => { describe('assigns obfuscation metadata', () => { @@ -20,7 +22,7 @@ describe('plugin obfuscation', () => { };`; test('[BABEL]', () => { - expect(babelTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` + expect(babelTransform(code, { EXPERIMENTAL_obfuscate: true })).toMatchInlineSnapshot(` "import { tgpu } from 'typegpu'; const external = { n: 1 @@ -46,7 +48,7 @@ describe('plugin obfuscation', () => { }); test('[ROLLUP]', async () => { - expect(await rollupTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` + expect(await rollupTransform(code, { EXPERIMENTAL_obfuscate: true })).toMatchInlineSnapshot(` "import 'typegpu'; const external = { n: 1 }; @@ -80,7 +82,7 @@ describe('plugin obfuscation', () => { }`; test('[BABEL]', () => { - expect(babelTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` + expect(babelTransform(code, { EXPERIMENTAL_obfuscate: true })).toMatchInlineSnapshot(` "import { tgpu } from 'typegpu'; export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = () => { const a = undefined; @@ -103,7 +105,7 @@ describe('plugin obfuscation', () => { }); test('[ROLLUP]', async () => { - expect(await rollupTransform(code, { obfuscate: true })).toMatchInlineSnapshot(` + expect(await rollupTransform(code, { EXPERIMENTAL_obfuscate: true })).toMatchInlineSnapshot(` "import 'typegpu'; const fn = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = (() => { @@ -119,6 +121,32 @@ describe('plugin obfuscation', () => { `); }); }); + + describe('conflicting options', () => { + test('[BABEL]', () => { + expect(() => + babelTransform('', { EXPERIMENTAL_obfuscate: true, autoNamingEnabled: true }), + ).toThrowErrorMatchingInlineSnapshot( + `[Error: unknown file: Options 'EXPERIMENTAL_obfuscate' and 'autoNamingEnabled' cannot be enabled at the same time.]`, + ); + }); + + test('[ROLLUP]', async () => { + expect(() => + rollupPlugin({ ...defaultOptions, EXPERIMENTAL_obfuscate: true, autoNamingEnabled: true }), + ).toThrowErrorMatchingInlineSnapshot( + `[Error: Options 'EXPERIMENTAL_obfuscate' and 'autoNamingEnabled' cannot be enabled at the same time.]`, + ); + }); + + test('[BUN]', async () => { + expect(() => + bunPlugin({ autoNamingEnabled: true, EXPERIMENTAL_obfuscate: true }), + ).toThrowErrorMatchingInlineSnapshot( + `[Error: Options 'EXPERIMENTAL_obfuscate' and 'autoNamingEnabled' cannot be enabled at the same time.]`, + ); + }); + }); }); // We only test tinyest -> tinyest transformation. From b8bed322912c659a0852340ccd6200b629849886 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:38:03 +0200 Subject: [PATCH 27/29] Add docs --- apps/typegpu-docs/astro.config.mjs | 5 +++ .../docs/advanced/minifying-shaders.mdx | 33 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 apps/typegpu-docs/src/content/docs/advanced/minifying-shaders.mdx diff --git a/apps/typegpu-docs/astro.config.mjs b/apps/typegpu-docs/astro.config.mjs index 1ac7e12b31..40c95c2865 100644 --- a/apps/typegpu-docs/astro.config.mjs +++ b/apps/typegpu-docs/astro.config.mjs @@ -215,6 +215,11 @@ export default defineConfig({ label: 'Timing Your Pipelines', slug: 'advanced/timestamp-queries', }, + { + label: 'Minifying & Obfuscating Shaders', + slug: 'advanced/minifying-shaders', + badge: { text: 'new' } + }, DEV && { label: 'Naming Convention', slug: 'advanced/naming-convention', diff --git a/apps/typegpu-docs/src/content/docs/advanced/minifying-shaders.mdx b/apps/typegpu-docs/src/content/docs/advanced/minifying-shaders.mdx new file mode 100644 index 0000000000..b77944629e --- /dev/null +++ b/apps/typegpu-docs/src/content/docs/advanced/minifying-shaders.mdx @@ -0,0 +1,33 @@ +--- +title: Minifying & Obfuscating Shaders +description: How to setup TypeGPU so that resulting shaders will be smaller or obfuscated. +--- + +:::caution[Experimental] +This feature is under development and is yet to reach stability. +::: + +Regular JS minification & obfuscation does not result in changes to the shader code generated by TypeGPU. +This is why we provide specific options for transforming shaders. + +## Plugin obfuscation + +:::note +To reduce both the shader size and readability, it is advised to disable the plugin auto-naming by setting `{ autoNamingEnabled: false }`. +This way, only resources given name via `.$name()` will be named in the resulting shader. +::: + +`unplugin-typegpu` collects function metadata, which is later used for WGSL code generation. +With the `{ EXPERIMENTAL_obfuscate: true }` option, all saved identifiers will be obfuscated: +- In the AST, all parameters and variables will have their names changed to `a`, `b`, `c`, ... +- Externals (the captured scope used by the function) will also have their respective identifiers changed. + +:::caution +Enabling this will obfuscate not only the resulting code, but also error messages appearing during resolution. + +It is not advised to minify shaders during development. +::: + +## Runtime minification + +Coming soon. From c9ad0483555aa87b5d47f10dde60ea039b4980cd Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:41:35 +0200 Subject: [PATCH 28/29] nr fix --- apps/typegpu-docs/astro.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/typegpu-docs/astro.config.mjs b/apps/typegpu-docs/astro.config.mjs index 40c95c2865..ba7d9af1ea 100644 --- a/apps/typegpu-docs/astro.config.mjs +++ b/apps/typegpu-docs/astro.config.mjs @@ -218,7 +218,7 @@ export default defineConfig({ { label: 'Minifying & Obfuscating Shaders', slug: 'advanced/minifying-shaders', - badge: { text: 'new' } + badge: { text: 'new' }, }, DEV && { label: 'Naming Convention', From a590a52fceb9fe53ad62d58d97813d982886b814 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:43:25 +0200 Subject: [PATCH 29/29] Remove outdated test --- packages/unplugin-typegpu/test/obfuscation.test.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index f967bcc259..734c925118 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -574,16 +574,4 @@ describe('obfuscate', () => { expect(stringifiedBody).toContain('ab'); expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); - - it('omits reserved words', () => { - const code = `() => { ${Array.from({ length: 26 + 26 * 26 }, (_, i) => `let v${i};`).join('\n')} }`; - const transpiled = transpileFn(parse(code)); - const { params, body, externalNames } = obfuscate(transpiled); - - expect(params).toMatchInlineSnapshot(`[]`); - const stringifiedBody = stringifyNode(body); - expect(stringifiedBody).not.toContain('if'); - expect(stringifiedBody).toContain('aaa'); - expect(externalNames).toMatchInlineSnapshot(`Map {}`); - }); });