Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
0b5f005
ast parser
cieplypolar Aug 14, 2026
b8432e6
minor cleanup
cieplypolar Sep 7, 2026
ccc9ce0
func body and params tests
cieplypolar Sep 14, 2026
7200055
babel evaluate instead of parsing
cieplypolar Sep 15, 2026
0e1183c
throwing errors instead of returning undefined
cieplypolar Sep 15, 2026
89e0d46
new shape
cieplypolar Sep 21, 2026
dbbc5d2
parsing externals
cieplypolar Sep 22, 2026
792b519
nullish operator fallback
cieplypolar Sep 22, 2026
d4f0d66
update docs
cieplypolar Sep 22, 2026
b694ed3
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Sep 22, 2026
4fcb009
typo
cieplypolar Sep 22, 2026
2671548
polish
cieplypolar Sep 22, 2026
5bc5fbd
one more test
cieplypolar Sep 22, 2026
20f961b
review changes
cieplypolar Sep 22, 2026
6bd5476
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Sep 24, 2026
9f07e01
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Sep 24, 2026
552f5fa
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Sep 24, 2026
e2ec6fa
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Sep 25, 2026
434d5bf
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Sep 25, 2026
09d5961
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Sep 28, 2026
f8ac486
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Sep 28, 2026
ade3b74
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Sep 29, 2026
7162f53
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Sep 29, 2026
cb55f2d
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Sep 30, 2026
adb9e99
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 1, 2026
52e298c
cyclic dep
cieplypolar Oct 1, 2026
a9c4715
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 1, 2026
391741f
better js doc
cieplypolar Oct 1, 2026
7b6d756
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 5, 2026
639dbbc
review fixes
cieplypolar Oct 5, 2026
7df7581
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 7, 2026
5d95cb5
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 12, 2026
6db5081
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 13, 2026
06df8bd
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 14, 2026
258c1fd
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 15, 2026
eda9a58
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 15, 2026
5f9605e
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 15, 2026
105f764
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 16, 2026
1791677
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 19, 2026
30a64bd
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 21, 2026
e0aa245
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 23, 2026
f3d4725
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 23, 2026
3d80ab6
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 26, 2026
e5722c6
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 26, 2026
05be5b2
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 28, 2026
5208e45
fix unwrapping metadata
cieplypolar Oct 29, 2026
444f4ff
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 30, 2026
c17c69a
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Oct 30, 2026
99777d9
lowering arrow functions
cieplypolar Oct 30, 2026
2c724e5
Merge branch 'main' into impr/unplugin-ast-ext-parser
cieplypolar Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/unplugin-typegpu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,11 @@
"unplugin": "^3.0.0"
},
"devDependencies": {
"@babel/generator": "^7.29.0",
"@babel/standalone": "^7.28.6",
"@rollup/plugin-virtual": "^3.0.2",
"@typegpu/tgpu-dev-cli": "workspace:*",
"@types/babel__generator": "^7.27.0",
"@types/babel__standalone": "^7.1.9",
"@types/babel__traverse": "^7.20.7",
"@types/bun": "^1.3.11",
Expand Down
2 changes: 1 addition & 1 deletion packages/unplugin-typegpu/src/babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { NodePath, TraverseOptions } from '@babel/traverse';
import defu from 'defu';
import type { Externals, TranspilationResult } from 'tinyest-for-wgsl';
import * as t from '@babel/types';
import { METADATA_FORMAT_VERSION } from './core/version.ts';
import {
METADATA_FORMAT_VERSION,
type PluginState,
checkOpts,
defaultOptions,
Expand Down
6 changes: 0 additions & 6 deletions packages/unplugin-typegpu/src/core/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ import { transpileBabelFn, type TranspilationResult } from 'tinyest-for-wgsl';
import { getEmbeddedTypegpuMetadata } from './embeddedMetadata.ts';
import { obfuscate } from './obfuscate.ts';

/**
* Each breaking change to the metadata format requires a bump to this number.
* It's used at runtime by `typegpu` to determine how to interpret a function's metadata.
*/
export const METADATA_FORMAT_VERSION = 2;

export interface Options {
/** @default [/\.m?[jt]sx?$/] */
include?: FilterPattern;
Expand Down
194 changes: 149 additions & 45 deletions packages/unplugin-typegpu/src/core/embeddedMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import type { NodePath } from '@babel/traverse';
import * as t from '@babel/types';
import { type MetadatableFunction } from './common.ts';
import type { TranspilationResult } from 'tinyest-for-wgsl';
import { METADATA_FORMAT_VERSION } from './version.ts';
import type { MetadatableFunction } from './common.ts';

export interface EmbeddedTypegpuMetadata {
const METADATA_PARSING_ERROR_MESSAGE =
'[unplugin-typegpu] Error when parsing metadata: required fields are missing or could not be evaluated.';

type FunctionAst = Pick<TranspilationResult, 'params' | 'body'>;
type FunctionExternals = Map<string, NodePath<t.ObjectProperty>>;

export type EmbeddedTypegpuMetadata = {
v: number;
name: string | undefined;
// TODO: parse AST and externals
}
function?: {
ast: FunctionAst;
astPath: NodePath<t.ObjectExpression>;
Comment thread
cieplypolar marked this conversation as resolved.
Resolved
externals: FunctionExternals;
};
};

const embeddedTypegpuMetadataCache = new WeakMap<
NodePath<MetadatableFunction>,
Expand Down Expand Up @@ -118,46 +130,100 @@ function skipTransparentWrappersUp(path: NodePath): NodePath {
}

/**
* Returns the node of the property with the given name in the object, if it exists.
* Walks down from the given path through parentheses and type-only wrappers,
* returning the innermost wrapped expression.
*/
function skipTransparentWrappersDown(path: NodePath): NodePath {
let current = path;

while (isTransparentWrapper(current.node)) {
current = current.get('expression') as NodePath;
}

return current;
}

/**
* Returns the value path of the first matching property. If there is no match, returns `undefined`.
*/
function objectPropertyValue(object: t.ObjectExpression, expectedName: string): t.Node | undefined {
for (const property of object.properties) {
if (!t.isObjectProperty(property) || property.computed) {
function objectPropertyPath(
objectPath: NodePath<t.ObjectExpression>,
name: string,
): NodePath<t.Expression> | undefined {
for (const property of objectPath.get('properties')) {
if (!property.isObjectProperty()) {
continue;
}

const name = t.isIdentifier(property.key)
? property.key.name
: t.isStringLiteral(property.key)
? property.key.value
: undefined;
const { key, computed } = property.node;

if (name === expectedName) {
return property.value;
const matches =
(!computed && t.isIdentifier(key, { name })) || t.isStringLiteral(key, { value: name });

if (!matches) {
continue;
}

const value = property.get('value');
if (value.isExpression()) {
return value;
}
}

return undefined;
}
/**
* Returns the parsed AST
*/
function parseAstPath(astPath: NodePath<t.ObjectExpression>): FunctionAst | undefined {
const evaluated = astPath.evaluate();

return !evaluated.confident ? undefined : (evaluated.value as FunctionAst);
}

/**
* Externals are represented as an object with string keys and external getters.
* This function parses such objects and returns a map keyed by string, with values that point to the corresponding object property node paths.
*/
function parseExternalsPath(
externalsPath: NodePath<t.ObjectExpression>,
): FunctionExternals | undefined {
const externals: FunctionExternals = new Map();

for (const propertyPath of externalsPath.get('properties')) {
if (!propertyPath.isObjectProperty()) {
return undefined;
}

const { computed, key } = propertyPath.node;

const name = t.isStringLiteral(key)
? key.value
: !computed && t.isIdentifier(key)
? key.name
: undefined;

if (name === undefined) {
return undefined;
}

externals.set(name, propertyPath);
}

return externals;
}

/**
* Returns metadata embedded by unplugin-typegpu for this exact function.
* Externals are required as part of the emitted shape, but are intentionally not evaluated.
*
* Consider:
* ```ts
* const f = ($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = () => {
* 'use gpu';
* }, {
* v: 2,
* name: "f",
* ast: {
* params: [],
* body: [0, []]
* },
* externals: {}
* }) && $.f)({});
* ```
* The returned object contains:
* - `v`: the metadata version
* - `name`: the function name, which may be undefined
* - `function`: an object containing the parsed AST, its source node path, and a map
* from external names to their original property paths
*
* @note Support for metadata stamped with a version different from `METADATA_FORMAT_VERSION` is limited.
* Only the version and name are parsed.
*/
export function getEmbeddedTypegpuMetadata(
path: NodePath<MetadatableFunction>,
Expand Down Expand Up @@ -190,31 +256,69 @@ export function getEmbeddedTypegpuMetadata(
}

// we check for the metadata object
const metadataNode = callPath.node.arguments[1];
if (!t.isObjectExpression(metadataNode)) {
const metadataPath = callPath.get('arguments.1');
if (metadataPath === undefined) {
return undefined;
}

const unwrappedMetadataPath = skipTransparentWrappersDown(metadataPath as NodePath);
if (!unwrappedMetadataPath.isObjectExpression()) {
return undefined;
}

// get the metadata properties
const versionNode = objectPropertyValue(metadataNode, 'v');
const nameNode = objectPropertyValue(metadataNode, 'name');
const astNode = objectPropertyValue(metadataNode, 'ast');
const externalsNode = objectPropertyValue(metadataNode, 'externals');
const versionPath = objectPropertyPath(unwrappedMetadataPath, 'v');
const namePath = objectPropertyPath(unwrappedMetadataPath, 'name');

if (
!t.isNumericLiteral(versionNode) ||
nameNode === undefined ||
astNode === undefined ||
externalsNode === undefined
) {
return undefined;
if (versionPath === undefined || namePath === undefined) {
throw new Error(METADATA_PARSING_ERROR_MESSAGE);
}

const versionResult = versionPath.evaluate();
const nameResult = namePath.evaluate();

if (!versionResult.confident || !nameResult.confident) {
throw new Error(METADATA_PARSING_ERROR_MESSAGE);
}
Comment thread
cieplypolar marked this conversation as resolved.
Resolved

const version = versionResult.value as number;
const name = nameResult.value as string | undefined;

if (version !== METADATA_FORMAT_VERSION) {
const embeddedTypegpuMetadata = {
v: version,
name,
};
embeddedTypegpuMetadataCache.set(path, embeddedTypegpuMetadata);
return embeddedTypegpuMetadata;
}

const name = t.isStringLiteral(nameNode) ? nameNode.value : undefined;
const astPath = objectPropertyPath(unwrappedMetadataPath, 'ast');
const externalsPath = objectPropertyPath(unwrappedMetadataPath, 'externals');

if (astPath === undefined || externalsPath === undefined) {
throw new Error(METADATA_PARSING_ERROR_MESSAGE);
}

if (!astPath.isObjectExpression() || !externalsPath.isObjectExpression()) {
throw new Error(METADATA_PARSING_ERROR_MESSAGE);
}

const ast = parseAstPath(astPath);
const externals = parseExternalsPath(externalsPath);

if (ast === undefined || externals === undefined) {
throw new Error(METADATA_PARSING_ERROR_MESSAGE);
}

const embeddedTypegpuMetadata = {
v: versionNode.value,
v: version,
name,
function: {
ast,
astPath,
externals,
},
};
embeddedTypegpuMetadataCache.set(path, embeddedTypegpuMetadata);

Expand Down
2 changes: 1 addition & 1 deletion packages/unplugin-typegpu/src/core/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import _traverse, { type NodePath } from '@babel/traverse';
import type { Externals, TranspilationResult } from 'tinyest-for-wgsl';
import * as parser from '@babel/parser';
import * as t from '@babel/types';
import { METADATA_FORMAT_VERSION } from './version.ts';
import {
defaultOptions,
earlyPruneRegex,
initPluginState,
functionVisitor,
getBlockScope,
METADATA_FORMAT_VERSION,
checkOpts,
} from './common.ts';

Expand Down
5 changes: 5 additions & 0 deletions packages/unplugin-typegpu/src/core/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Each breaking change to the metadata format requires a bump to this number.
* It's used at runtime by `typegpu` to determine how to interpret a function's metadata.
*/
export const METADATA_FORMAT_VERSION = 2;
Loading
Loading