UNPKG

unplugin-typegpu

Version:

Build plugins for TypeGPU, enabling seamless JavaScript -> WGSL transpilation and improved debugging.

1 lines 13.7 kB
{"version":3,"sources":["../src/babel.ts","../src/common.ts"],"names":["defaultOptions","embedJSON","jsValue","isTgpu","ctx","node","path","tail"],"mappings":"AAAA,whBAAuB,kCAIQ,kDACH,ICiBfA,CAAAA,CAAiB,CAC5B,OAAA,CAAS,CAAC,cAAc,CAAA,CACxB,iBAAA,CAAmB,CAAA,CACrB,CAAA,CAEO,SAASC,CAAAA,CAAUC,CAAAA,CAAkB,CAC1C,OAAO,IAAA,CAAK,SAAA,CAAUA,CAAO,CAAA,CAC1B,OAAA,CAAQ,SAAA,CAAW,SAAS,CAAA,CAC5B,OAAA,CAAQ,SAAA,CAAW,SAAS,CACjC,CAMA,SAASC,CAAAA,CAAOC,CAAAA,CAAcC,CAAAA,CAA2C,CACvE,IAAIC,CAAAA,CAAO,EAAA,CAEPC,CAAAA,CAAOF,CAAAA,CACX,GAAA,CAAA,CAAA,CAAA,CACE,EAAA,CAAIE,CAAAA,CAAK,IAAA,GAAS,kBAAA,CAAoB,CACpC,EAAA,CAAA,CACGA,CAAAA,CAAK,QAAA,CAAS,IAAA,GAAS,SAAA,EACtBA,CAAAA,CAAK,QAAA,CAAS,IAAA,GAAS,eAAA,CAAA,EACzBA,CAAAA,CAAK,QAAA,CAAS,KAAA,GAAU,WAAA,CACxB,CAEAA,CAAAA,CAAOA,CAAAA,CAAK,MAAA,CACZ,QACF,CAEA,EAAA,CAAIA,CAAAA,CAAK,QAAA,CAAS,IAAA,GAAS,YAAA,CAEzB,KAAA,CAGFD,CAAAA,CAAOA,CAAAA,CAAO,CAAA,EAAA;ADVG,OAAA;AAC8B,SAAA;AACX,gBAAA;AAuCpC,GAAA","file":"/Users/konradreczko/TypeGPU/wigsill/packages/unplugin-typegpu/dist/chunk-4DTTWQTW.cjs","sourcesContent":["import * as Babel from '@babel/standalone';\nimport type TemplateGenerator from '@babel/template';\nimport type { TraverseOptions } from '@babel/traverse';\nimport type * as babel from '@babel/types';\nimport { FORMAT_VERSION } from 'tinyest';\nimport { transpileFn } from 'tinyest-for-wgsl';\nimport {\n type Context,\n embedJSON,\n gatherTgpuAliases,\n isShellImplementationCall,\n kernelDirective,\n type Options,\n performExpressionNaming,\n} from './common.ts';\nimport { createFilterForId } from './filter.ts';\n\n// NOTE: @babel/standalone does expose internal packages, as specified in the docs, but the\n// typing for @babel/standalone does not expose them.\nconst template = (\n Babel as unknown as { packages: { template: typeof TemplateGenerator } }\n).packages.template;\nconst types = (Babel as unknown as { packages: { types: typeof babel } })\n .packages.types;\n\nfunction containsKernelDirective(\n node:\n | babel.FunctionDeclaration\n | babel.FunctionExpression\n | babel.ArrowFunctionExpression,\n): boolean {\n return ((\n 'directives' in node.body ? (node.body?.directives ?? []) : []\n )\n .map((directive) => directive.value.value))\n .includes(kernelDirective);\n}\n\nfunction i(identifier: string): babel.Identifier {\n return types.identifier(identifier);\n}\n\nfunction functionToTranspiled(\n node: babel.ArrowFunctionExpression | babel.FunctionExpression,\n): babel.CallExpression {\n const { params, body, externalNames } = transpileFn(node);\n\n const metadata = `{\n v: ${FORMAT_VERSION},\n ast: ${embedJSON({ params, body, externalNames })},\n externals: {${externalNames.join(', ')}},\n }`;\n\n return types.callExpression(\n types.arrowFunctionExpression(\n [i('$')],\n types.logicalExpression(\n '&&',\n types.callExpression(\n types.memberExpression(\n types.assignmentExpression(\n '??=',\n types.memberExpression(i('globalThis'), i('__TYPEGPU_META__')),\n types.newExpression(i('WeakMap'), []),\n ),\n i('set'),\n ),\n [\n types.assignmentExpression(\n '=',\n types.memberExpression(i('$'), i('f')),\n node,\n ),\n template.expression`${metadata}`(),\n ],\n ),\n types.memberExpression(i('$'), i('f')),\n ),\n ),\n [types.objectExpression([])],\n );\n}\n\nfunction wrapInAutoName(\n node: babel.Expression,\n name: string,\n) {\n return types.callExpression(\n template.expression('globalThis.__TYPEGPU_AUTONAME__ ?? (a => a)', {\n placeholderPattern: false,\n })(),\n [node, types.stringLiteral(name)],\n );\n}\n\nfunction functionVisitor(ctx: Context): TraverseOptions {\n return {\n VariableDeclarator(path) {\n performExpressionNaming(ctx, path.node, (node, name) => {\n path.get('init').replaceWith(wrapInAutoName(node, name));\n });\n },\n\n AssignmentExpression(path) {\n performExpressionNaming(ctx, path.node, (node, name) => {\n path.get('right').replaceWith(wrapInAutoName(node, name));\n });\n },\n\n ObjectProperty(path) {\n performExpressionNaming(ctx, path.node, (node, name) => {\n path.get('value').replaceWith(wrapInAutoName(node, name));\n });\n },\n\n ImportDeclaration(path) {\n gatherTgpuAliases(path.node, ctx);\n },\n\n ArrowFunctionExpression(path) {\n if (containsKernelDirective(path.node)) {\n path.replaceWith(functionToTranspiled(path.node));\n path.skip();\n }\n },\n\n FunctionExpression(path) {\n if (containsKernelDirective(path.node)) {\n path.replaceWith(functionToTranspiled(path.node));\n path.skip();\n }\n },\n\n FunctionDeclaration(path) {\n const node = path.node;\n const expression = types.functionExpression(\n node.id,\n node.params,\n node.body,\n );\n\n if (containsKernelDirective(path.node) && node.id) {\n const transpiled = functionToTranspiled(expression);\n path.replaceWith(\n types.variableDeclaration('const', [\n types.variableDeclarator(node.id, transpiled),\n ]),\n );\n path.skip();\n }\n },\n\n CallExpression(path) {\n const node = path.node;\n\n if (isShellImplementationCall(node, ctx)) {\n const implementation = node.arguments[0];\n\n if (\n implementation &&\n (implementation.type === 'FunctionExpression' ||\n implementation.type === 'ArrowFunctionExpression')\n ) {\n const transpiled = functionToTranspiled(\n implementation,\n ) as babel.CallExpression;\n\n path.replaceWith(\n types.callExpression(node.callee, [\n transpiled,\n ]),\n );\n\n path.skip();\n }\n }\n },\n };\n}\n\nexport default function () {\n return {\n visitor: {\n Program(path, state) {\n // biome-ignore lint/suspicious/noExplicitAny: <oh babel babel...>\n const code: string | undefined = (state as any).file?.code;\n // biome-ignore lint/suspicious/noExplicitAny: <oh babel babel...>\n const options: Options | undefined = (state as any).opts;\n // biome-ignore lint/suspicious/noExplicitAny: <oh babel babel...>\n const id: string | undefined = (state as any).filename;\n\n const filter = createFilterForId(options);\n if (id && filter && !filter?.(id)) {\n return;\n }\n\n const ctx: Context = {\n tgpuAliases: new Set<string>(\n options?.forceTgpuAlias ? [options.forceTgpuAlias] : [],\n ),\n fileId: id,\n autoNamingEnabled: options?.autoNamingEnabled ?? true,\n };\n\n path.traverse(functionVisitor(ctx));\n },\n } satisfies TraverseOptions,\n };\n}\n","import type * as babel from '@babel/types';\nimport type * as acorn from 'acorn';\nimport type { FilterPattern } from 'unplugin';\n\nexport type Context = {\n /**\n * How the `tgpu` object is used in code. Since it can be aliased, we\n * need to catch that and act accordingly.\n */\n tgpuAliases: Set<string>;\n fileId?: string | undefined;\n autoNamingEnabled: boolean;\n};\n\nexport interface Options {\n include?: FilterPattern;\n exclude?: FilterPattern;\n enforce?: 'post' | 'pre' | undefined;\n forceTgpuAlias?: string;\n autoNamingEnabled?: boolean;\n}\n\nexport const defaultOptions = {\n include: [/\\.m?[jt]sx?$/],\n autoNamingEnabled: true,\n};\n\nexport function embedJSON(jsValue: unknown) {\n return JSON.stringify(jsValue)\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029');\n}\n\n/**\n * Checks if `node` is an alias for the 'tgpu' object, traditionally\n * available via `import tgpu from 'typegpu'`.\n */\nfunction isTgpu(ctx: Context, node: babel.Node | acorn.AnyNode): boolean {\n let path = '';\n\n let tail = node;\n while (true) {\n if (tail.type === 'MemberExpression') {\n if (\n (tail.property.type === 'Literal' ||\n tail.property.type === 'StringLiteral') &&\n tail.property.value === '~unstable'\n ) {\n // Bypassing the '~unstable' property.\n tail = tail.object;\n continue;\n }\n\n if (tail.property.type !== 'Identifier') {\n // Not handling computed expressions.\n break;\n }\n\n path = path ? `${tail.property.name}.${path}` : tail.property.name;\n tail = tail.object;\n } else if (tail.type === 'Identifier') {\n path = path ? `${tail.name}.${path}` : tail.name;\n break;\n } else {\n break;\n }\n }\n\n return ctx.tgpuAliases.has(path);\n}\n\nexport function gatherTgpuAliases(\n node: acorn.ImportDeclaration | babel.ImportDeclaration,\n ctx: Context,\n) {\n if (node.source.value === 'typegpu') {\n for (const spec of node.specifiers) {\n if (\n // The default export of 'typegpu' is the `tgpu` object.\n spec.type === 'ImportDefaultSpecifier' ||\n // Aliasing 'tgpu' while importing, e.g. import { tgpu as t } from 'typegpu';\n (spec.type === 'ImportSpecifier' &&\n spec.imported.type === 'Identifier' &&\n spec.imported.name === 'tgpu')\n ) {\n ctx.tgpuAliases.add(spec.local.name);\n } else if (spec.type === 'ImportNamespaceSpecifier') {\n // Importing everything, e.g. import * as t from 'typegpu';\n ctx.tgpuAliases.add(`${spec.local.name}.tgpu`);\n }\n }\n }\n}\n\nconst fnShellFunctionNames = ['fn', 'vertexFn', 'fragmentFn', 'computeFn'];\n\nexport function isShellImplementationCall(\n node: acorn.CallExpression | babel.CallExpression,\n ctx: Context,\n) {\n return (\n node.callee.type === 'CallExpression' &&\n node.callee.callee.type === 'MemberExpression' &&\n node.callee.callee.property.type === 'Identifier' &&\n fnShellFunctionNames.includes(node.callee.callee.property.name) &&\n node.arguments.length === 1 && isTgpu(ctx, node.callee.callee.object)\n );\n}\n\nconst resourceConstructors: string[] = [\n // tgpu\n 'bindGroupLayout',\n 'vertexLayout',\n // tgpu['~unstable']\n 'slot',\n 'accessor',\n 'privateVar',\n 'workgroupVar',\n 'const',\n ...fnShellFunctionNames,\n // d\n 'struct',\n 'unstruct',\n // root\n 'createBuffer',\n 'createMutable',\n 'createReadonly',\n 'createUniform',\n 'createQuerySet',\n // root['~unstable']\n 'createPipeline',\n 'createTexture',\n 'sampler',\n 'comparisonSampler',\n];\n\n/**\n * Checks if `node` should be wrapped in an autoname function.\n * Since it is mostly for debugging and clean WGSL generation,\n * some false positives and false negatives are admissible.\n */\nfunction containsResourceConstructorCall(\n node: acorn.AnyNode | babel.Node,\n ctx: Context,\n) {\n if (node.type === 'CallExpression') {\n if (isShellImplementationCall(node, ctx)) {\n return true;\n }\n // struct({...})\n if (\n node.callee.type === 'Identifier' &&\n resourceConstructors.includes(node.callee.name)\n ) {\n return true;\n }\n if (node.callee.type === 'MemberExpression') {\n if (node.callee.property.type === 'Identifier') {\n // root.createBuffer({...})\n if (resourceConstructors.includes(node.callee.property.name)) {\n return true;\n }\n if (node.callee.property.name === '$name') {\n return false;\n }\n }\n // root.createBuffer(d.f32).$usage('storage')\n return containsResourceConstructorCall(node.callee.object, ctx);\n }\n }\n if (node.type === 'TaggedTemplateExpression') {\n return containsResourceConstructorCall(node.tag, ctx);\n }\n return false;\n}\n\ntype ExpressionFor<T extends acorn.AnyNode | babel.Node> = T extends\n acorn.AnyNode ? acorn.Expression : babel.Expression;\n\n/**\n * Checks if `node` contains a label and a tgpu expression that could be named.\n * If so, it calls the provided callback. Nodes selected for naming include:\n *\n * `let name = tgpu.bindGroupLayout({});` (VariableDeclarator)\n *\n * `name = tgpu.bindGroupLayout({});` (AssignmentExpression)\n *\n * `property: tgpu.bindGroupLayout({})` (Property/ObjectProperty)\n *\n * Since it is mostly for debugging and clean WGSL generation,\n * some false positives and false negatives are admissible.\n *\n * @privateRemarks\n * When adding new checks, you need to call this method in the corresponding node in Babel.\n */\nexport function performExpressionNaming<T extends acorn.AnyNode | babel.Node>(\n ctx: Context,\n node: T,\n namingCallback: (node: ExpressionFor<T>, name: string) => void,\n) {\n if (!ctx.autoNamingEnabled) {\n return;\n }\n\n if (\n node.type === 'VariableDeclarator' &&\n node.id.type === 'Identifier' &&\n node.init &&\n containsResourceConstructorCall(node.init, ctx)\n ) {\n namingCallback(node.init as ExpressionFor<T>, node.id.name);\n } else if (\n node.type === 'AssignmentExpression' &&\n node.left.type === 'Identifier' &&\n containsResourceConstructorCall(node.right, ctx)\n ) {\n namingCallback(node.right as ExpressionFor<T>, node.left.name);\n } else if (\n (node.type === 'Property' || node.type === 'ObjectProperty') &&\n node.key.type === 'Identifier' &&\n containsResourceConstructorCall(node.value, ctx)\n ) {\n namingCallback(node.value as ExpressionFor<T>, node.key.name);\n }\n}\n\nexport const kernelDirective = 'kernel';\n"]}