From 976af867b5750fe592bc7d63ceee5e2b53933af0 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 28 Oct 2026 14:38:32 +0100 Subject: [PATCH 1/6] obfuscator rework --- .../src/core/embeddedMetadata.ts | 16 +++++++----- .../unplugin-typegpu/src/core/obfuscate.ts | 26 ++++++++++++++++--- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/unplugin-typegpu/src/core/embeddedMetadata.ts b/packages/unplugin-typegpu/src/core/embeddedMetadata.ts index 5651bf1858..0666c79dff 100644 --- a/packages/unplugin-typegpu/src/core/embeddedMetadata.ts +++ b/packages/unplugin-typegpu/src/core/embeddedMetadata.ts @@ -7,17 +7,19 @@ import type { MetadatableFunction } from './common.ts'; const METADATA_PARSING_ERROR_MESSAGE = '[unplugin-typegpu] Error when parsing metadata: required fields are missing or could not be evaluated.'; -type FunctionAst = Pick; -type FunctionExternals = Map>; +export type FunctionAst = Pick; +export type FunctionExternals = Map>; + +type EmbeddedFunctionMetadata = { + ast: FunctionAst; + astPath: NodePath; + externals: FunctionExternals; +}; export type EmbeddedTypegpuMetadata = { v: number; name: string | undefined; - function?: { - ast: FunctionAst; - astPath: NodePath; - externals: FunctionExternals; - }; + function?: EmbeddedFunctionMetadata; }; const embeddedTypegpuMetadataCache = new WeakMap< diff --git a/packages/unplugin-typegpu/src/core/obfuscate.ts b/packages/unplugin-typegpu/src/core/obfuscate.ts index 462dd5233d..fa4804e165 100644 --- a/packages/unplugin-typegpu/src/core/obfuscate.ts +++ b/packages/unplugin-typegpu/src/core/obfuscate.ts @@ -1,5 +1,7 @@ import type { TranspilationResult } from 'tinyest-for-wgsl'; import * as tinyest from 'tinyest'; +import type { FunctionAst } from './embeddedMetadata.ts'; + const { NodeTypeCatalog: NODE } = tinyest; /** @@ -60,7 +62,12 @@ class Context { } } -export function obfuscate(fn: TranspilationResult): TranspilationResult { +type ObfuscationResult = { + ast: FunctionAst; + externalKeyMap: Map; +}; + +export function obfuscateAst(fn: FunctionAst, externalKeys: Iterable): ObfuscationResult { const ctx = new Context(); const params = fn.params.map((param) => { @@ -76,10 +83,21 @@ export function obfuscate(fn: TranspilationResult): TranspilationResult { const body = obf(ctx, fn.body); - const externalNames = new Map(); - fn.externalNames.forEach((value, key) => externalNames.set(ctx.obfuscator.obfuscate(key), value)); + const externalKeyMap = new Map( + Array.from(externalKeys, (key) => [key, ctx.obfuscator.obfuscate(key)] as const), + ); + + return { ast: { params, body }, externalKeyMap }; +} + +export function obfuscate(fn: TranspilationResult): TranspilationResult { + const { ast, externalKeyMap } = obfuscateAst(fn, fn.externalNames.keys()); + + const externalNames = new Map( + Array.from(fn.externalNames, ([key, value]) => [externalKeyMap.get(key) as string, value]), + ); - return { ...fn, params, body, externalNames }; + return { ...fn, ...ast, externalNames }; } // Nodes like 'continue' and 'break' are still listed From e95403826ae47718f080f0b6d57f401728b416de Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Wed, 28 Oct 2026 17:49:10 +0100 Subject: [PATCH 2/6] reobfuscation --- packages/unplugin-typegpu/src/babel.ts | 18 ++++++ packages/unplugin-typegpu/src/core/common.ts | 64 +++++++++++++++++-- packages/unplugin-typegpu/src/core/factory.ts | 17 ++++- 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/packages/unplugin-typegpu/src/babel.ts b/packages/unplugin-typegpu/src/babel.ts index 733cfb41a7..19e79e1bdc 100644 --- a/packages/unplugin-typegpu/src/babel.ts +++ b/packages/unplugin-typegpu/src/babel.ts @@ -11,6 +11,7 @@ import { getBlockScope, initPluginState, } from './core/common.ts'; +import type { FunctionAst, FunctionExternals } from './core/embeddedMetadata.ts'; import { createFilterForId } from './core/filter.ts'; function i(identifier: string): t.Identifier { @@ -126,6 +127,22 @@ function assignMetadata( path.skip(); } +function replaceWithObfuscated( + this: PluginState, + astPath: NodePath, + obfuscatedAst: FunctionAst, + embeddedExternals: FunctionExternals, + externalKeyToObfuscatedMap: Map, +): void { + astPath.replaceWith(t.valueToNode(obfuscatedAst)); + + for (const [key, propertyPath] of embeddedExternals) { + propertyPath + .get('key') + .replaceWith(t.stringLiteral(externalKeyToObfuscatedMap.get(key) as string)); + } +} + function wrapInAutoName(path: NodePath, name: string): void { // /*#__PURE__*/(globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(, '') const callExpr = t.callExpression( @@ -169,6 +186,7 @@ export default function TypeGPUPlugin() { initPluginState(this, { warn: (message) => console.warn(message), assignMetadata, + replaceWithObfuscated, wrapInAutoName, replaceWithAssignmentOverload, replaceWithBinaryOverload, diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index d6e449cfd9..e4953f112f 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -3,8 +3,14 @@ import type { NodePath, TraverseOptions } from '@babel/traverse'; import type { FilterPattern } from 'unplugin'; import MagicString from 'magic-string'; import { transpileBabelFn, type TranspilationResult } from 'tinyest-for-wgsl'; +import type { + EmbeddedTypegpuMetadata, + FunctionAst, + FunctionExternals, +} from './embeddedMetadata.ts'; import { getEmbeddedTypegpuMetadata } from './embeddedMetadata.ts'; -import { obfuscate } from './obfuscate.ts'; +import { obfuscate, obfuscateAst } from './obfuscate.ts'; +import { METADATA_FORMAT_VERSION } from './version.ts'; export interface Options { /** @default [/\.m?[jt]sx?$/] */ @@ -93,6 +99,19 @@ export interface TransformMethods { ast: TranspilationResult, ): void; + /** + * Replaces the embedded AST and the keys of the externals object of an + * already transformed function with their obfuscated versions. + * Externals' values (getters) are left untouched. + */ + replaceWithObfuscated( + this: PluginState, + astPath: NodePath, + obfuscatedAst: FunctionAst, + embeddedExternals: FunctionExternals, + externalKeyToObfuscatedMap: Map, + ): void; + wrapInAutoName(this: PluginState, path: NodePath, name: string): void; replaceWithAssignmentOverload(path: NodePath, runtimeFn: string): void; @@ -120,6 +139,9 @@ export interface PluginState extends TransformMethods { opts: Required; inUseGpuScope: boolean; + + /** Used to warn about unsupported metadata versions at most once per file. */ + warnedAboutMetadataVersion: boolean; } export interface NodeLocation { @@ -138,11 +160,12 @@ export function initPluginState(state: PluginState, methods: TransformMethods): state.tgpuAliases = new Set(state.opts.forceTgpuAlias ? [state.opts.forceTgpuAlias] : []); state.autoNamingEnabled = state.opts.autoNamingEnabled ?? true; state.inUseGpuScope = false; + state.warnedAboutMetadataVersion = false; Object.assign(state, methods); } /** Regular expressions used for early pruning (to avoid unnecessary parsing, which is expensive) */ -export const earlyPruneRegex = [/["']use gpu["']/, /t(ype)?gpu/]; +export const earlyPruneRegex = [/["']use gpu["']/, /t(ype)?gpu/, /__TYPEGPU_META__/]; export const defaultOptions = { include: /\.m?[jt]sx?(?:\?.*)?$/, @@ -463,12 +486,39 @@ const fnNodeToTranspiledMap = new WeakMap< TranspilationResult >(); -function functionOnExit( - path: NodePath, - state: PluginState, -) { +function reobfuscateEmbedded(state: PluginState, metadata: EmbeddedTypegpuMetadata) { + if (metadata.v === METADATA_FORMAT_VERSION && metadata.function !== undefined) { + const { ast, astPath, externals } = metadata.function; + const obfuscated = obfuscateAst(ast, externals.keys()); + state.replaceWithObfuscated(astPath, obfuscated.ast, externals, obfuscated.externalKeyMap); + return; + } + + if (state.warnedAboutMetadataVersion) { + return; + } + + state.warnedAboutMetadataVersion = true; + state.warn( + `[unplugin-typegpu] Cannot obfuscate already transformed functions in ${state.filename ?? ''}: unsupported metadata version ${metadata.v} (expected ${METADATA_FORMAT_VERSION}).`, + ); +} + +function functionOnExit(path: NodePath, state: PluginState) { const node = path.node; - if (!containsUseGpuDirective(node) || getEmbeddedTypegpuMetadata(path) !== undefined) { + const hasDirective = containsUseGpuDirective(node); + const obfuscate = state.opts.unstable_obfuscate; + + if (!hasDirective && !obfuscate) { + return; + } + + const metadata = getEmbeddedTypegpuMetadata(path); + if (metadata !== undefined && obfuscate) { + reobfuscateEmbedded(state, metadata); + } + + if (!hasDirective || metadata !== undefined) { return; } diff --git a/packages/unplugin-typegpu/src/core/factory.ts b/packages/unplugin-typegpu/src/core/factory.ts index 5a4445fdb9..414bd0b811 100644 --- a/packages/unplugin-typegpu/src/core/factory.ts +++ b/packages/unplugin-typegpu/src/core/factory.ts @@ -15,8 +15,8 @@ import { getBlockScope, checkOpts, } from './common.ts'; - import type { Options, UnpluginPluginState, MetadatableFunction, NodeLocation } from './common.ts'; +import type { FunctionAst, FunctionExternals } from './embeddedMetadata.ts'; // I love CommonJS 💔 let traverse = _traverse; @@ -97,6 +97,20 @@ function assignMetadata( } } +function replaceWithObfuscated( + this: UnpluginPluginState, + astPath: NodePath, + obfuscatedAst: FunctionAst, + embeddedExternals: FunctionExternals, + externalKeyToObfuscatedMap: Map, +): void { + this.overwrite(astPath.node, embedJSON(obfuscatedAst)); + + for (const [key, propertyPath] of embeddedExternals) { + this.overwrite(propertyPath.node.key, `"${externalKeyToObfuscatedMap.get(key)}"`); + } +} + function wrapInAutoName( this: UnpluginPluginState, path: NodePath, @@ -188,6 +202,7 @@ export const unpluginFactory = ((rawOptions, _meta) => { initPluginState(state, { warn: (message) => this.warn(message), assignMetadata, + replaceWithObfuscated, wrapInAutoName, replaceWithAssignmentOverload, replaceWithBinaryOverload, From 0edee82edd8b42607a264c9d68a240af9bc0ba87 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Thu, 29 Oct 2026 13:16:37 +0100 Subject: [PATCH 3/6] early pruning regex update --- packages/unplugin-typegpu/src/bun.ts | 3 ++- packages/unplugin-typegpu/src/core/common.ts | 8 +++++++- packages/unplugin-typegpu/src/core/factory.ts | 4 ++-- packages/unplugin-typegpu/src/node.ts | 3 ++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/unplugin-typegpu/src/bun.ts b/packages/unplugin-typegpu/src/bun.ts index fbf04dff02..c563ce4c1e 100644 --- a/packages/unplugin-typegpu/src/bun.ts +++ b/packages/unplugin-typegpu/src/bun.ts @@ -1,10 +1,11 @@ import defu from 'defu'; -import { checkOpts, defaultOptions, earlyPruneRegex, type Options } from './core/common.ts'; +import { checkOpts, defaultOptions, getEarlyPruneRegex, 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 = checkOpts(defu(rawOptions, defaultOptions)); + const earlyPruneRegex = getEarlyPruneRegex(options.unstable_obfuscate); 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 e4953f112f..4cfec7fd9d 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -165,7 +165,13 @@ export function initPluginState(state: PluginState, methods: TransformMethods): } /** Regular expressions used for early pruning (to avoid unnecessary parsing, which is expensive) */ -export const earlyPruneRegex = [/["']use gpu["']/, /t(ype)?gpu/, /__TYPEGPU_META__/]; +export const getEarlyPruneRegex = (obfuscate: boolean) => { + const baseRegex = [/["']use gpu["']/, /t(ype)?gpu/]; + if (obfuscate) { + baseRegex.push(/__TYPEGPU_META__/); + } + return baseRegex; +}; export const defaultOptions = { include: /\.m?[jt]sx?(?:\?.*)?$/, diff --git a/packages/unplugin-typegpu/src/core/factory.ts b/packages/unplugin-typegpu/src/core/factory.ts index 414bd0b811..bfc37ec4e7 100644 --- a/packages/unplugin-typegpu/src/core/factory.ts +++ b/packages/unplugin-typegpu/src/core/factory.ts @@ -9,7 +9,7 @@ import * as t from '@babel/types'; import { METADATA_FORMAT_VERSION } from './version.ts'; import { defaultOptions, - earlyPruneRegex, + getEarlyPruneRegex, initPluginState, functionVisitor, getBlockScope, @@ -166,7 +166,7 @@ export const unpluginFactory = ((rawOptions, _meta) => { filter: options.earlyPruning ? { id: options, - code: earlyPruneRegex, + code: getEarlyPruneRegex(options.unstable_obfuscate), } : { id: options, diff --git a/packages/unplugin-typegpu/src/node.ts b/packages/unplugin-typegpu/src/node.ts index a1aab01694..624559da89 100644 --- a/packages/unplugin-typegpu/src/node.ts +++ b/packages/unplugin-typegpu/src/node.ts @@ -2,7 +2,7 @@ import { registerHooks, type ModuleHooks } from 'node:module'; import { fileURLToPath } from 'node:url'; import remapping from '@jridgewell/remapping'; import type { UnpluginBuildContext, UnpluginContext } from 'unplugin'; -import { earlyPruneRegex, type Options } from './core/common.ts'; +import { getEarlyPruneRegex, type Options } from './core/common.ts'; import { unpluginFactory } from './core/factory.ts'; import { createFilterForId } from './core/filter.ts'; @@ -52,6 +52,7 @@ export default function install(rawOptions?: Options): ModuleHooks { { framework: 'unloader' }, ); const filter = createFilterForId(plugin.transform.filter.id); + const earlyPruneRegex = getEarlyPruneRegex(rawOptions?.unstable_obfuscate ?? false); const transformContext = { warn: (message: string) => console.warn(`[unplugin-typegpu] ${message}`), } as UnpluginBuildContext & UnpluginContext; From 7f870f2498e75861f94f20a9ea07962ebb31c7ac Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Thu, 29 Oct 2026 13:31:29 +0100 Subject: [PATCH 4/6] tests --- .../unplugin-typegpu/test/obfuscation.test.ts | 236 +++++++++++++++++- 1 file changed, 235 insertions(+), 1 deletion(-) diff --git a/packages/unplugin-typegpu/test/obfuscation.test.ts b/packages/unplugin-typegpu/test/obfuscation.test.ts index aa69082b59..2c661fc628 100644 --- a/packages/unplugin-typegpu/test/obfuscation.test.ts +++ b/packages/unplugin-typegpu/test/obfuscation.test.ts @@ -1,6 +1,6 @@ import type { ArrowFunctionExpression } from '@babel/types'; import { transpileBabelFn } from 'tinyest-for-wgsl'; -import { describe, expect, it, test } from 'vitest'; +import { describe, expect, it, test, vi } from 'vitest'; import { obfuscate } from '../src/core/obfuscate.ts'; import babelParser from '@babel/parser'; import { stringifyNode } from 'typegpu/~internal'; @@ -127,6 +127,233 @@ describe('plugin obfuscation', () => { }); }); + describe('obfuscates built shellless functions', () => { + const code = `\ + const external = { n: 1 } + + export const fn = (argument) => { + 'use gpu'; + const variable = 3; + return external.n + argument + variable; + };`; + + test('[BABEL]', () => { + const built = babelTransform(code, { unstable_obfuscate: false }) as string; + expect(babelTransform(built, { unstable_obfuscate: true })).toMatchInlineSnapshot(` + "const external = { + n: 1 + }; + export const fn = /*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = argument => { + 'use gpu'; + + 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 () => { + const built = await rollupTransform(code, { unstable_obfuscate: false }); + expect(await rollupTransform(built, { unstable_obfuscate: true })).toMatchInlineSnapshot(` + "const external = { n: 1 }; + + const fn = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = ((argument) => { + 'use gpu'; + 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('obfuscates built shelled functions', () => { + const code = `\ + import { tgpu, d } from 'typegpu'; + + const external = { n: 1 } + + export const fn = tgpu.fn([d.f32], d.f32)((argument) => { + const variable = 3; + return external.n + argument + variable; + });`; + + test('[BABEL]', () => { + const built = babelTransform(code, { unstable_obfuscate: false }) as string; + expect(babelTransform(built, { unstable_obfuscate: true })).toMatchInlineSnapshot(` + "import { tgpu, d } from 'typegpu'; + const external = { + n: 1 + }; + export const fn = tgpu.fn([d.f32], d.f32)(/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = argument => { + const variable = 3; + return external.n + argument + variable; + }, { + v: 2, + name: undefined, + 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 () => { + const built = await rollupTransform(code, { unstable_obfuscate: false }); + expect(await rollupTransform(built, { unstable_obfuscate: true })).toMatchInlineSnapshot(` + "import { tgpu, d } from 'typegpu'; + + const external = { n: 1 }; + + const fn = tgpu.fn([d.f32], d.f32)((/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = ((argument) => { + const variable = 3; + return external.n + argument + variable; + }), { + v: 2, + name: undefined, + 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('is idempotent', () => { + const code = `\ + const external = { n: 1 } + + export const fn = (argument) => { + 'use gpu'; + const variable = 3; + return external.n + argument + variable; + };`; + + test('[BABEL]', () => { + const once = babelTransform(code, { unstable_obfuscate: true }) as string; + const twice = babelTransform(once, { unstable_obfuscate: true }); + expect(twice).toBe(once); + }); + + test('[ROLLUP]', async () => { + const once = await rollupTransform(code, { unstable_obfuscate: true }); + const twice = await rollupTransform(once, { unstable_obfuscate: true }); + expect(twice).toBe(once); + }); + }); + + describe('warns once per file about unsupported metadata versions', () => { + const code = `\ + export const a = () => { + 'use gpu'; + }; + export const b = () => { + 'use gpu'; + };`; + + test('[BABEL]', () => { + using warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const built = babelTransform(code, { unstable_obfuscate: false }) as string; + const unsupported = built.replace(/\bv: \d+/g, 'v: 1'); + const result = babelTransform(unsupported, { unstable_obfuscate: true }); + + expect(result).toBe(babelTransform(unsupported, { unstable_obfuscate: false })); + expect(warn.mock.calls).toHaveLength(1); + expect(warn.mock.calls[0]?.[0]).toMatch( + /Cannot obfuscate already transformed functions.*unsupported metadata version 1 \(expected 2\)/, + ); + }); + + test('[ROLLUP]', async () => { + using warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const built = await rollupTransform(code, { unstable_obfuscate: false }); + const unsupported = built.replace(/\bv: \d+/g, 'v: 1'); + const result = await rollupTransform(unsupported, { unstable_obfuscate: true }); + + expect(result).toBe(await rollupTransform(unsupported, { unstable_obfuscate: false })); + expect(warn.mock.calls).toHaveLength(1); + expect(warn.mock.calls[0]?.[0]).toMatch( + /Cannot obfuscate already transformed functions.*unsupported metadata version 1 \(expected 2\)/, + ); + }); + }); + + test('does not prune files that only contain embedded metadata when obfuscation is on', async () => { + const code = `\ + const external = { n: 1 }; + export const fn = () => { + 'use gpu'; + return external.n; + };`; + + const built = await rollupTransform(code, { unstable_obfuscate: false }); + + const withoutDirective = built.replace(/^[ \t]*['"]use gpu['"];?\r?\n/gm, ''); + expect(withoutDirective).not.toMatch(/use gpu|t(ype)?gpu/); + + expect(await rollupTransform(withoutDirective, { unstable_obfuscate: true })) + .toMatchInlineSnapshot(` + "const external = { n: 1 }; + const fn = (/*#__PURE__*/($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = (() => { + return external.n; + }), { + v: 2, + name: "fn", + ast: {"params":[],"body":[0,[[10,"a"]]]}, + externals: {"a":() => external.n} + }) && $.f)({})); + + export { fn }; + " + `); + }); + + test('prunes files that only contain embedded metadata when obfuscation is off', async () => { + const code = `\ + export const fn = () => { + 'use gpu'; + return 42; + };`; + + const built = await rollupTransform(code, { unstable_obfuscate: false }); + const withoutDirective = built.replace(/^[ \t]*['"]use gpu['"];?\r?\n/gm, ''); + + using parse = vi.spyOn(babelParser, 'parse'); + await rollupTransform(withoutDirective, { unstable_obfuscate: false }); + expect(parse).not.toHaveBeenCalled(); + }); + describe('conflicting options', () => { test('[BABEL]', () => { expect(() => @@ -191,6 +418,13 @@ describe('obfuscate', () => { expect(externalNames).toMatchInlineSnapshot(`Map {}`); }); + it('keeps the source map', () => { + const code = `() => { const variable = 1; return variable; }`; + const transpiled = transpileBabelFn(parse(code)); + + expect(obfuscate(transpiled).sourceMap).toBe(transpiled.sourceMap); + }); + it('remembers obfuscated names', () => { const code = `() => { const variable = 1; return variable; }`; const transpiled = transpileBabelFn(parse(code)); From 14d13c7d83e0b0a763b655db5329f18b34fd37a3 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Thu, 29 Oct 2026 16:26:11 +0100 Subject: [PATCH 5/6] update docs --- .../src/content/docs/advanced/minifying-shaders.mdx | 10 +++++++++- .../src/content/docs/tooling/unplugin-typegpu.mdx | 6 +++--- packages/unplugin-typegpu/src/core/common.ts | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/typegpu-docs/src/content/docs/advanced/minifying-shaders.mdx b/apps/typegpu-docs/src/content/docs/advanced/minifying-shaders.mdx index 0a53de3e81..143a2248c0 100644 --- a/apps/typegpu-docs/src/content/docs/advanced/minifying-shaders.mdx +++ b/apps/typegpu-docs/src/content/docs/advanced/minifying-shaders.mdx @@ -53,6 +53,14 @@ fn item(a: f32, b: f32) -> f32 { Obfuscation does not affect raw-WGSL implemented functions. ::: +:::caution[Experimental] +`unplugin-typegpu` also supports obfuscating sources that have already been built with the plugin. + +Process those files again with `{ unstable_obfuscate: true, autoNamingEnabled: false }` to obfuscate their embedded shader metadata. + +Names added during the original build are preserved, including those added with `{ autoNamingEnabled: true }`. +::: + ## Removing whitespaces in runtime @@ -86,4 +94,4 @@ fn coneVolume(radius:f32,height:f32)->f32{let baseArea=(3.141592653589793f*pow(r Of course, minification and obfuscation can be combined to achieve the following: ```wgsl fn item(a:f32,b:f32)->f32{let c=(3.141592653589793f*pow(a,2f));let e=((c*b)/3f);return e;} -``` \ No newline at end of file +``` diff --git a/apps/typegpu-docs/src/content/docs/tooling/unplugin-typegpu.mdx b/apps/typegpu-docs/src/content/docs/tooling/unplugin-typegpu.mdx index 081781bc61..fc05a3f742 100644 --- a/apps/typegpu-docs/src/content/docs/tooling/unplugin-typegpu.mdx +++ b/apps/typegpu-docs/src/content/docs/tooling/unplugin-typegpu.mdx @@ -182,9 +182,9 @@ interface Options { autoNamingEnabled?: boolean; /** - * Skipping files that don't contain 'typegpu', 'tgpu' or 'use gpu'. - * In case this early pruning hinders transformation, you - * can disable it. + * Skipping files that don't contain "typegpu", "tgpu" or "use gpu". + * When `unstable_obfuscate` is enabled, files containing `__TYPEGPU_META__` are also processed. + * In case this early pruning hinders transformation, you can disable it. * * @default true */ diff --git a/packages/unplugin-typegpu/src/core/common.ts b/packages/unplugin-typegpu/src/core/common.ts index 4cfec7fd9d..4f5b6a3e66 100644 --- a/packages/unplugin-typegpu/src/core/common.ts +++ b/packages/unplugin-typegpu/src/core/common.ts @@ -38,8 +38,8 @@ export interface Options { /** * Skipping files that don't contain "typegpu", "tgpu" or "use gpu". - * In case this early pruning hinders transformation, you - * can disable it. + * When `unstable_obfuscate` is enabled, files containing `__TYPEGPU_META__` are also processed. + * In case this early pruning hinders transformation, you can disable it. * * @default true */ From f2ac18368117cbb8f40395096d893b6c1be7a8d6 Mon Sep 17 00:00:00 2001 From: Szymon Szulc Date: Fri, 30 Oct 2026 15:46:22 +0100 Subject: [PATCH 6/6] bun warn fix --- packages/unplugin-typegpu/src/bun.ts | 3 +- packages/unplugin-typegpu/test/bun.test.ts | 33 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/unplugin-typegpu/src/bun.ts b/packages/unplugin-typegpu/src/bun.ts index c563ce4c1e..ea0b7df8e3 100644 --- a/packages/unplugin-typegpu/src/bun.ts +++ b/packages/unplugin-typegpu/src/bun.ts @@ -33,7 +33,8 @@ export default (rawOptions?: Options): Bun.BunPlugin => { } const result = rawPlugin.transform.handler.apply( - {} as UnpluginBuildContext & UnpluginContext, + { warn: (message: string) => console.warn(message) } as UnpluginBuildContext & + UnpluginContext, [codeIn, args.path], ); diff --git a/packages/unplugin-typegpu/test/bun.test.ts b/packages/unplugin-typegpu/test/bun.test.ts index 374e52cd5f..cdbd70d30b 100644 --- a/packages/unplugin-typegpu/test/bun.test.ts +++ b/packages/unplugin-typegpu/test/bun.test.ts @@ -1,5 +1,6 @@ import { afterEach, assert, expect, it, vi } from 'vitest'; import bunPlugin from '../src/bun.ts'; +import { babelTransform } from './transform.ts'; afterEach(() => { vi.unstubAllGlobals(); @@ -29,3 +30,35 @@ it.each([undefined, true, false])('respects earlyPruning: %s', async (earlyPruni } expect(result.loader).toBe('ts'); }); + +it('warns once and preserves unsupported metadata when obfuscating', async () => { + using warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const built = babelTransform(` + export const a = () => { 'use gpu'; }; + export const b = () => { 'use gpu'; }; + `) as string; + const code = built.replace(/\bv: \d+/g, 'v: 1'); + + vi.stubGlobal('Bun', { file: () => ({ text: async () => code }) }); + const onLoad = vi.fn(); + await bunPlugin({ unstable_obfuscate: true, autoNamingEnabled: false }).setup({ + onLoad, + } as unknown as Bun.PluginBuilder); + + const call = onLoad.mock.calls[0]; + assert(call); + const result = await call[1]({ + path: '/test.ts', + loader: 'ts', + namespace: 'file', + async defer() {}, + }); + assert(result && 'contents' in result); + + expect(result.contents).toBe(code); + expect(warn.mock.calls.length).toBe(1); + expect(warn.mock.calls[0]![0]).toMatchInlineSnapshot( + `"[unplugin-typegpu] Cannot obfuscate already transformed functions in /test.ts: unsupported metadata version 1 (expected 2)."`, + ); +});