diff --git a/apps/typegpu-docs/astro.config.mjs b/apps/typegpu-docs/astro.config.mjs index 72ae05ec7..86add74f6 100644 --- a/apps/typegpu-docs/astro.config.mjs +++ b/apps/typegpu-docs/astro.config.mjs @@ -27,11 +27,27 @@ const DEV = import.meta.env.DEV; export default defineConfig({ site: 'https://docs.swmansion.com', base: 'TypeGPU', + server: { + // Required for '@rolldown/browser' to work in dev mode. + // Since the service worker is hosted on the /TypeGPU path, + // fetches from /@fs/ fail due to CORS. This fixes that. + headers: { + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Opener-Policy': 'same-origin', + }, + }, markdown: { remarkPlugins: [remarkMath], rehypePlugins: [rehypeMathJax], }, vite: { + define: { + // Required for '@rolldown/browser' to work. + 'process.env.NODE_DEBUG_NATIVE': '""', + }, + optimizeDeps: { + exclude: ['@rolldown/browser'], + }, // Allowing query params, for invalidation plugins: [ wasm(), @@ -48,6 +64,7 @@ export default defineConfig({ ssr: { noExternal: [ 'wgsl-wasm-transpiler-bundler', + '@rolldown/browser', ], }, }, diff --git a/apps/typegpu-docs/package.json b/apps/typegpu-docs/package.json index bc6ceaa21..921436768 100644 --- a/apps/typegpu-docs/package.json +++ b/apps/typegpu-docs/package.json @@ -22,6 +22,7 @@ "@monaco-editor/react": "^4.7.0", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slider": "^1.3.5", + "@rolldown/browser": "1.0.0-beta.43", "@stackblitz/sdk": "^1.11.0", "@tailwindcss/vite": "^4.1.13", "@typegpu/color": "workspace:*", diff --git a/apps/typegpu-docs/public/coi-serviceworker.js b/apps/typegpu-docs/public/coi-serviceworker.js new file mode 100644 index 000000000..22091f9ff --- /dev/null +++ b/apps/typegpu-docs/public/coi-serviceworker.js @@ -0,0 +1,175 @@ +/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ + +// This service worker is responsible for intercepting fetch requests to +// assets hosted on the same origin, and attaching CORS headers that +// allow SharedArrayBuffer to function (required by @rolldown/browser). + +let coepCredentialless = false; +if (typeof window === 'undefined') { + self.addEventListener('install', () => self.skipWaiting()); + self.addEventListener( + 'activate', + (event) => event.waitUntil(self.clients.claim()), + ); + + self.addEventListener('message', (ev) => { + if (!ev.data) { + return; + } + + if (ev.data.type === 'deregister') { + self.registration + .unregister() + .then(() => { + return self.clients.matchAll(); + }) + .then((clients) => { + for (const client of clients) { + client.navigate(client.url); + } + }); + } else if (ev.data.type === 'coepCredentialless') { + coepCredentialless = ev.data.value; + } + }); + + self.addEventListener('fetch', (event) => { + const r = event.request; + if (r.cache === 'only-if-cached' && r.mode !== 'same-origin') { + return; + } + + const request = (coepCredentialless && r.mode === 'no-cors') + ? new Request(r, { + credentials: 'omit', + }) + : r; + event.respondWith( + fetch(request) + .then((response) => { + if (response.status === 0) { + return response; + } + + const newHeaders = new Headers(response.headers); + newHeaders.set( + 'Cross-Origin-Embedder-Policy', + coepCredentialless ? 'credentialless' : 'require-corp', + ); + if (!coepCredentialless) { + newHeaders.set('Cross-Origin-Resource-Policy', 'cross-origin'); + } + newHeaders.set('Cross-Origin-Opener-Policy', 'same-origin'); + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: newHeaders, + }); + }) + .catch((e) => console.error(e)), + ); + }); +} else { + (() => { + const reloadedBySelf = window.sessionStorage.getItem('coiReloadedBySelf'); + window.sessionStorage.removeItem('coiReloadedBySelf'); + const coepDegrading = reloadedBySelf === 'coepdegrade'; + + // You can customize the behavior of this script through a global `coi` variable. + const coi = { + shouldRegister: () => !reloadedBySelf, + shouldDeregister: () => false, + coepCredentialless: () => true, + coepDegrade: () => true, + doReload: () => window.location.reload(), + quiet: false, + ...window.coi, + }; + + const n = navigator; + const controlling = !!n.serviceWorker && !!n.serviceWorker.controller; + + // Record the failure if the page is served by serviceWorker. + if (controlling && !window.crossOriginIsolated) { + window.sessionStorage.setItem('coiCoepHasFailed', 'true'); + } + const coepHasFailed = window.sessionStorage.getItem('coiCoepHasFailed'); + + if (controlling) { + // Reload only on the first failure. + const reloadToDegrade = coi.coepDegrade() && !( + coepDegrading || window.crossOriginIsolated + ); + n.serviceWorker.controller.postMessage({ + type: 'coepCredentialless', + value: (reloadToDegrade || coepHasFailed && coi.coepDegrade()) + ? false + : coi.coepCredentialless(), + }); + if (reloadToDegrade) { + !coi.quiet && console.log('Reloading page to degrade COEP.'); + window.sessionStorage.setItem('coiReloadedBySelf', 'coepdegrade'); + coi.doReload('coepdegrade'); + } + + if (coi.shouldDeregister()) { + n.serviceWorker.controller.postMessage({ type: 'deregister' }); + } + } + + // If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are + // already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here. + if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return; + + if (!window.isSecureContext) { + !coi.quiet && + console.log( + 'COOP/COEP Service Worker not registered, a secure context is required.', + ); + return; + } + + // In some environments (e.g. Firefox private mode) this won't be available + if (!n.serviceWorker) { + !coi.quiet && + console.error( + 'COOP/COEP Service Worker not registered, perhaps due to private mode.', + ); + return; + } + + n.serviceWorker.register(window.document.currentScript.src).then( + (registration) => { + !coi.quiet && + console.log( + 'COOP/COEP Service Worker registered', + registration.scope, + ); + + registration.addEventListener('updatefound', () => { + !coi.quiet && + console.log( + 'Reloading page to make use of updated COOP/COEP Service Worker.', + ); + window.sessionStorage.setItem('coiReloadedBySelf', 'updatefound'); + coi.doReload(); + }); + + // If the registration is active, but it's not controlling the page + if (registration.active && !n.serviceWorker.controller) { + !coi.quiet && + console.log( + 'Reloading page to make use of COOP/COEP Service Worker.', + ); + window.sessionStorage.setItem('coiReloadedBySelf', 'notcontrolling'); + coi.doReload(); + } + }, + (err) => { + !coi.quiet && + console.error('COOP/COEP Service Worker failed to register:', err); + }, + ); + })(); +} diff --git a/apps/typegpu-docs/src/components/CodeEditor.tsx b/apps/typegpu-docs/src/components/CodeEditor.tsx index a086536cb..aadbc2531 100644 --- a/apps/typegpu-docs/src/components/CodeEditor.tsx +++ b/apps/typegpu-docs/src/components/CodeEditor.tsx @@ -17,7 +17,7 @@ function handleEditorWillMount(monaco: Monaco) { entries(SANDBOX_MODULES), map(([key, moduleDef]) => { if ('reroute' in moduleDef.typeDef) { - return [key, moduleDef.typeDef.reroute] as const; + return [key, [moduleDef.typeDef.reroute]] as [string, string[]]; } return null; }), diff --git a/apps/typegpu-docs/src/components/SearchableExampleList.tsx b/apps/typegpu-docs/src/components/SearchableExampleList.tsx index 2418cf0c6..65106ad33 100644 --- a/apps/typegpu-docs/src/components/SearchableExampleList.tsx +++ b/apps/typegpu-docs/src/components/SearchableExampleList.tsx @@ -12,8 +12,8 @@ function ExamplesGrid({ examples }: { examples: Example[] }) { ); } -const DEV = globalThis.process.env.NODE_ENV === 'development'; -const TEST = globalThis.process.env.NODE_ENV === 'test'; +const DEV = process.env.NODE_ENV === 'development'; +const TEST = process.env.NODE_ENV === 'test'; export function SearchableExampleList( { excludeTags = [], scrollContainerRef }: { diff --git a/apps/typegpu-docs/src/components/translator/lib/constants.ts b/apps/typegpu-docs/src/components/translator/lib/constants.ts index 313bd47a1..de5e2b5da 100644 --- a/apps/typegpu-docs/src/components/translator/lib/constants.ts +++ b/apps/typegpu-docs/src/components/translator/lib/constants.ts @@ -23,7 +23,6 @@ fn fs_main() -> @location(0) vec4 { export const DEFAULT_TGSL = `import tgpu from 'typegpu'; import * as d from 'typegpu/data'; -import * as std from 'typegpu/std'; const Particle = d.struct({ position: d.vec3f, @@ -42,14 +41,9 @@ const layout = tgpu.bindGroupLayout({ export const updateParicle = tgpu.fn([Particle, d.vec3f, d.f32], Particle)( (particle, gravity, deltaTime) => { - const newVelocity = std.mul( - particle.velocity, - std.mul(gravity, deltaTime), - ); - const newPosition = std.add( - particle.position, - std.mul(newVelocity, deltaTime), - ); + const newVelocity = particle.velocity.mul(gravity).mul(deltaTime); + const newPosition = particle.position.add(newVelocity.mul(deltaTime)); + return Particle({ position: newPosition, velocity: newVelocity, diff --git a/apps/typegpu-docs/src/components/translator/lib/editorConfig.ts b/apps/typegpu-docs/src/components/translator/lib/editorConfig.ts index 10450ad6b..90a8c5464 100644 --- a/apps/typegpu-docs/src/components/translator/lib/editorConfig.ts +++ b/apps/typegpu-docs/src/components/translator/lib/editorConfig.ts @@ -44,7 +44,7 @@ export function setupMonacoEditor(monaco: Monaco) { entries(SANDBOX_MODULES), map(([key, moduleDef]) => { if ('reroute' in moduleDef.typeDef) { - return [key, moduleDef.typeDef.reroute] as const; + return [key, [moduleDef.typeDef.reroute]] as [string, string[]]; } return null; }), diff --git a/apps/typegpu-docs/src/components/translator/lib/rolldown.ts b/apps/typegpu-docs/src/components/translator/lib/rolldown.ts new file mode 100644 index 000000000..cd8114184 --- /dev/null +++ b/apps/typegpu-docs/src/components/translator/lib/rolldown.ts @@ -0,0 +1,99 @@ +import type { InputOptions, OutputOptions } from '@rolldown/browser'; +import { join } from 'pathe'; + +export interface BundleResult { + output: Record; + warnings?: string[] | undefined; +} + +export interface SourceFile { + filename: string; + code: string; + isEntry?: boolean; +} + +export type FileMap = Record< + string, + { + content: string; + } | { + reroute: string; + } | undefined +>; + +export async function bundle( + files: FileMap, + entries: string[], + config: InputOptions & { output?: OutputOptions | undefined } = {}, +): Promise { + const rolldown = await import('@rolldown/browser'); + + const warnings: string[] = []; + + const inputOptions: InputOptions = { + input: entries, + cwd: '/', + onLog(level, log, logger) { + if (level === 'warn') { + warnings.push(String(log)); + } else { + logger(level, log); + } + }, + ...config, + plugins: [ + // Virtual file system plugin + { + name: 'virtual-fs', + resolveId(source, importer) { + const id = source[0] === '.' + ? join(importer || '/', '..', source) + : source; + + if (files[id] && 'reroute' in files[id]) { + // Rerouting + return files[id].reroute; + } + + return id; + }, + load(id) { + if (!files[id]) { + return; + } + + if ('reroute' in files[id]) { + // Reroutes are supposed to be resolved in `resolveId` + throw new Error(`Unresolved reroute for ${id}`); + } + + return files[id].content; + }, + }, + ...(Array.isArray(config?.plugins) + ? config.plugins + : [config?.plugins].filter(Boolean)), + ], + }; + + const outputOptions: OutputOptions = { + format: 'esm', + ...config?.output, + }; + + const bundle = await rolldown.rolldown(inputOptions); + const result = await bundle.generate(outputOptions); + + const output = Object.fromEntries( + result.output.map((chunk) => + chunk.type === 'chunk' + ? [chunk.fileName, chunk.code] + : [chunk.fileName, chunk.source] + ), + ); + + return { + output, + warnings, + }; +} diff --git a/apps/typegpu-docs/src/components/translator/lib/tgslExecutor.ts b/apps/typegpu-docs/src/components/translator/lib/tgslExecutor.ts index 61e4f2ff8..5a222e985 100644 --- a/apps/typegpu-docs/src/components/translator/lib/tgslExecutor.ts +++ b/apps/typegpu-docs/src/components/translator/lib/tgslExecutor.ts @@ -1,30 +1,39 @@ -import * as Babel from '@babel/standalone'; -import plugin from 'unplugin-typegpu/babel'; - -function translateTGSL( - code: string, -): string { - const result = Babel.transform(code, { - 'presets': ['typescript'], - 'filename': 'example.ts', - plugins: [plugin], - }).code; - return result || ''; -} +import { mapValues, pipe } from 'remeda'; +import rolldownPlugin from 'unplugin-typegpu/rolldown-browser'; +import { bundle } from './rolldown.ts'; +import { SANDBOX_MODULES } from '../../../utils/examples/sandboxModules.ts'; const moduleImports = { - 'typegpu': 'https://esm.sh/typegpu@latest/?bundle=false', - 'typegpu/data': 'https://esm.sh/typegpu@latest/data/?bundle=false', - 'typegpu/std': 'https://esm.sh/typegpu@latest/std/?bundle=false', + 'typed-binary': 'https://esm.sh/typed-binary@latest', } as Record; type TgslModule = Record; async function executeTgslModule(tgslCode: string): Promise { - const translatedCode = translateTGSL(tgslCode); + const result = await bundle( + { + ...pipe(SANDBOX_MODULES, mapValues((val) => val.import)), + '/shader.ts': { content: tgslCode }, + '/index.ts': { + content: ` + import tgpu from 'typegpu'; + import * as exports from './shader.ts'; - const importMap = { imports: moduleImports }; + const shaderCode = tgpu.resolve({ externals: exports }); + export default shaderCode; + `, + }, + }, + ['./index.ts'], + { + plugins: [rolldownPlugin({})], + external: ['typed-binary'], + }, + ); + + const translatedCode = result.output['index.js']; + const importMap = { imports: moduleImports }; const importMapScript = document.createElement('script'); importMapScript.type = 'importmap'; importMapScript.textContent = JSON.stringify(importMap); @@ -34,7 +43,7 @@ async function executeTgslModule(tgslCode: string): Promise { const userBlob = new Blob([translatedCode], { type: 'text/javascript' }); const userModuleUrl = URL.createObjectURL(userBlob); - const module = await import(userModuleUrl); + const module = await import(/* @vite-ignore */ userModuleUrl); URL.revokeObjectURL(userModuleUrl); return module; @@ -45,17 +54,10 @@ async function executeTgslModule(tgslCode: string): Promise { export async function executeTgslCode(tgslCode: string): Promise { try { - const exports = await executeTgslModule(tgslCode); - - const tgpuModule = await import( - //@ts-expect-error - 'https://esm.sh/typegpu@latest?bundle=false' - ); - - return tgpuModule.default.resolve({ - externals: exports as Record, - }); + const shaderCode = await executeTgslModule(tgslCode); + return shaderCode.default as string; } catch (error) { + console.error(error); throw new Error( `Failed to execute TGSL code: ${ error instanceof Error ? error.message : String(error) diff --git a/apps/typegpu-docs/src/layouts/PageLayout.astro b/apps/typegpu-docs/src/layouts/PageLayout.astro index 395c14c0f..9ba9b4542 100644 --- a/apps/typegpu-docs/src/layouts/PageLayout.astro +++ b/apps/typegpu-docs/src/layouts/PageLayout.astro @@ -12,6 +12,7 @@ const { title, theme = 'light' } = Astro.props; + diff --git a/apps/typegpu-docs/src/utils/examples/sandboxModules.ts b/apps/typegpu-docs/src/utils/examples/sandboxModules.ts index 350e809dd..8d5b31987 100644 --- a/apps/typegpu-docs/src/utils/examples/sandboxModules.ts +++ b/apps/typegpu-docs/src/utils/examples/sandboxModules.ts @@ -4,7 +4,32 @@ import dtsWebGPU from '@webgpu/types/dist/index.d.ts?raw'; import dtsWgpuMatrix from 'wgpu-matrix/dist/3.x/wgpu-matrix.d.ts?raw'; interface SandboxModuleDefinition { - typeDef: { filename?: string; content: string } | { reroute: string[] }; + typeDef: + | { filename?: string; content: string } + | { reroute: string }; + import?: + | { filename?: string; content: string } + | { reroute: string } + | undefined; +} + +function srcFileToModule( + [filepath, content]: [string, string], + baseUrl: string, +): [moduleKey: string, moduleDef: SandboxModuleDefinition] { + const filename = filepath.replace(baseUrl, ''); + const def = { + filename, + content, + }; + + return [ + filename, + { + typeDef: def, + import: def, + }, + ] as const; } function dtsFileToModule( @@ -26,13 +51,16 @@ function dtsFileToModule( const allPackagesSrcFiles = pipe( entries( - import.meta.glob('../../../../../packages/*/src/**/*.ts', { + import.meta.glob([ + '../../../../../packages/*/src/**/*.ts', + '../../../../../packages/*/package.json', + ], { query: 'raw', eager: true, import: 'default', }) as Record, ), - map((dtsFile) => dtsFileToModule(dtsFile, '../../../../../packages/')), + map((dtsFile) => srcFileToModule(dtsFile, '../../../../../packages/')), fromEntries(), ); @@ -61,21 +89,30 @@ export const SANDBOX_MODULES: Record = { 'wgpu-matrix': { typeDef: { filename: 'wgpu-matrix.d.ts', content: dtsWgpuMatrix }, }, + tinyest: { + import: { reroute: 'tinyest/src/index.ts' }, + typeDef: { reroute: 'tinyest/src/index.ts' }, + }, typegpu: { - typeDef: { reroute: ['typegpu/src/index.ts'] }, + import: { reroute: 'typegpu/src/index.ts' }, + typeDef: { reroute: 'typegpu/src/index.ts' }, }, 'typegpu/data': { - typeDef: { reroute: ['typegpu/src/data/index.ts'] }, + import: { reroute: 'typegpu/src/data/index.ts' }, + typeDef: { reroute: 'typegpu/src/data/index.ts' }, }, 'typegpu/std': { - typeDef: { reroute: ['typegpu/src/std/index.ts'] }, + import: { reroute: 'typegpu/src/std/index.ts' }, + typeDef: { reroute: 'typegpu/src/std/index.ts' }, }, // Utility modules '@typegpu/noise': { - typeDef: { reroute: ['typegpu-noise/src/index.ts'] }, + import: { reroute: 'typegpu-noise/src/index.ts' }, + typeDef: { reroute: 'typegpu-noise/src/index.ts' }, }, '@typegpu/color': { - typeDef: { reroute: ['typegpu-color/src/index.ts'] }, + import: { reroute: 'typegpu-color/src/index.ts' }, + typeDef: { reroute: 'typegpu-color/src/index.ts' }, }, }; diff --git a/package.json b/package.json index cf2e300ff..92b0b95bb 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,11 @@ "type": "git", "url": "git+https://github.com/software-mansion/TypeGPU.git" }, - "keywords": ["webgpu", "wgpu", "wgsl"], + "keywords": [ + "webgpu", + "wgpu", + "wgsl" + ], "bugs": { "url": "https://github.com/software-mansion/TypeGPU/issues" }, @@ -52,6 +56,7 @@ "pnpm": { "onlyBuiltDependencies": [ "@biomejs/biome", + "@rolldown/browser", "@tailwindcss/oxide", "canvas", "esbuild", diff --git a/packages/typegpu/src/shared/env.ts b/packages/typegpu/src/shared/env.ts index d6c033c96..a0e05307d 100644 --- a/packages/typegpu/src/shared/env.ts +++ b/packages/typegpu/src/shared/env.ts @@ -5,6 +5,6 @@ * Even though the value of this constant uses Node.js specific APIs, pretty much every * bundler replaces the expression below with either `development` or `production` */ -export const DEV = globalThis.process.env.NODE_ENV === 'development'; +export const DEV = process.env.NODE_ENV === 'development'; -export const TEST = globalThis.process.env.NODE_ENV === 'test'; +export const TEST = process.env.NODE_ENV === 'test'; diff --git a/packages/unplugin-typegpu/package.json b/packages/unplugin-typegpu/package.json index 0480c26b5..de0489e8a 100644 --- a/packages/unplugin-typegpu/package.json +++ b/packages/unplugin-typegpu/package.json @@ -26,6 +26,7 @@ "./esbuild": "./src/esbuild.ts", "./farm": "./src/farm.ts", "./rolldown": "./src/rolldown.ts", + "./rolldown-browser": "./src/rolldown-browser.ts", "./rspack": "./src/rspack.ts", "./vite": "./src/vite.ts", "./webpack": "./src/webpack.ts" @@ -79,6 +80,12 @@ "import": "./dist/rolldown.js", "default": "./dist/rolldown.cjs" }, + "./rolldown-browser": { + "types": "./dist/rolldown-browser.d.ts", + "module": "./dist/rolldown-browser.js", + "import": "./dist/rolldown-browser.js", + "default": "./dist/rolldown-browser.cjs" + }, "./rspack": { "types": "./dist/rspack.d.ts", "module": "./dist/rspack.js", @@ -138,6 +145,7 @@ "@types/bun": "^1.2.22", "@types/picomatch": "^4.0.1", "acorn": "^8.14.1", + "rolldown": "1.0.0-beta.33", "rollup": "~4.37.0", "tsup": "catalog:build", "typescript": "catalog:types" diff --git a/packages/unplugin-typegpu/src/bun.ts b/packages/unplugin-typegpu/src/bun.ts index b7d9e1ebd..7080eafb0 100644 --- a/packages/unplugin-typegpu/src/bun.ts +++ b/packages/unplugin-typegpu/src/bun.ts @@ -17,7 +17,7 @@ export default (rawOptions: Options): BunPlugin => { } return { - name: 'TypeGPU', + name: 'unplugin-typegpu', setup(build) { build.onLoad({ filter: include }, async (args) => { const text = await Bun.file(args.path).text(); diff --git a/packages/unplugin-typegpu/src/index.ts b/packages/unplugin-typegpu/src/index.ts index 88c89d65a..9b5c06142 100644 --- a/packages/unplugin-typegpu/src/index.ts +++ b/packages/unplugin-typegpu/src/index.ts @@ -1,216 +1,7 @@ -import type * as acorn from 'acorn'; -import defu from 'defu'; -import { type Node, walk } from 'estree-walker'; -import { generateTransform, MagicStringAST } from 'magic-string-ast'; -import { FORMAT_VERSION } from 'tinyest'; -import { transpileFn } from 'tinyest-for-wgsl'; -import { createUnplugin, type UnpluginInstance } from 'unplugin'; -import { - type Context, - defaultOptions, - earlyPruneRegex, - embedJSON, - gatherTgpuAliases, - getFunctionName, - isShellImplementationCall, - type Options, - performExpressionNaming, - useGpuDirective, -} from './common.ts'; +import { createUnplugin } from 'unplugin'; +import { rollUpImpl } from './rollup-impl.ts'; -type FunctionNode = - | acorn.FunctionDeclaration - | acorn.AnonymousFunctionDeclaration - | acorn.FunctionExpression - | acorn.ArrowFunctionExpression; - -function containsUseGpuDirective(node: FunctionNode): boolean { - if (node.body.type === 'BlockStatement') { - for (const statement of node.body.body) { - if ( - statement.type === 'ExpressionStatement' && - statement.directive === useGpuDirective - ) { - return true; - } - } - } - return false; -} - -function removeUseGpuDirective(node: FunctionNode) { - const cloned = structuredClone(node); - - if (cloned.body.type === 'BlockStatement') { - cloned.body.body = cloned.body.body.filter( - (statement) => - !( - statement.type === 'ExpressionStatement' && - statement.directive === useGpuDirective - ), - ); - } - - return cloned; -} - -function assignMetadata( - magicString: MagicStringAST, - node: acorn.AnyNode, - metadata: string, -) { - magicString.prependLeft( - node.start, - '(($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = (', - ).appendRight( - node.end, - `), ${metadata}) && $.f)({}))`, - ); -} - -function wrapInAutoName( - magicString: MagicStringAST, - node: acorn.Node, - name: string, -) { - magicString - .prependLeft( - node.start, - '((globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(', - ) - .appendRight(node.end, `, "${name}"))`); -} - -const typegpu: UnpluginInstance = createUnplugin( - (rawOptions) => { - const options = defu(rawOptions, defaultOptions); - - return { - name: 'unplugin-typegpu' as const, - enforce: options.enforce, - transform: { - filter: options.earlyPruning - ? { - id: options, - code: earlyPruneRegex, - } - : { - id: options, - }, - handler(code, id) { - const ctx: Context = { - tgpuAliases: new Set( - options.forceTgpuAlias ? [options.forceTgpuAlias] : [], - ), - fileId: id, - autoNamingEnabled: options.autoNamingEnabled, - }; - - let ast: Node; - try { - ast = this.parse(code, { - lang: 'ts', - allowReturnOutsideFunction: true, - }) as Node; - } catch (cause) { - console.warn( - `[unplugin-typegpu] Failed to parse ${id}. Cause: ${ - typeof cause === 'object' && cause && 'message' in cause - ? cause.message - : cause - }`, - ); - return undefined; - } - - const tgslFunctionDefs: { - def: FunctionNode; - name?: string | undefined; - }[] = []; - - const magicString = new MagicStringAST(code); - - walk(ast, { - enter(_node, _parent, prop, index) { - const node = _node as acorn.AnyNode; - const parent = _parent as acorn.AnyNode; - - performExpressionNaming(ctx, node, (node, name) => { - wrapInAutoName(magicString, node, name); - }); - - if (node.type === 'ImportDeclaration') { - gatherTgpuAliases(node, ctx); - } - - if (node.type === 'CallExpression') { - if (isShellImplementationCall(node, ctx)) { - const implementation = node.arguments[0]; - - if ( - implementation && - (implementation.type === 'FunctionExpression' || - implementation.type === 'ArrowFunctionExpression') - ) { - tgslFunctionDefs.push({ - def: removeUseGpuDirective(implementation), - }); - this.skip(); - } - } - } - - if ( - node.type === 'ArrowFunctionExpression' || - node.type === 'FunctionExpression' || - node.type === 'FunctionDeclaration' - ) { - if (containsUseGpuDirective(node)) { - tgslFunctionDefs.push({ - def: removeUseGpuDirective(node), - name: getFunctionName(node, parent), - }); - this.skip(); - } - } - }, - }); - - for (const { def, name } of tgslFunctionDefs) { - const { params, body, externalNames } = transpileFn(def); - const isFunctionStatement = def.type === 'FunctionDeclaration'; - - if ( - isFunctionStatement && - name && - code.slice(0, def.start) - .search(new RegExp(`(? + !( + statement.type === 'ExpressionStatement' && + statement.directive === useGpuDirective + ), + ); + } + + return cloned; +} + +export function assignMetadata( + magicString: MagicStringAST, + node: acorn.AnyNode, + metadata: string, +) { + magicString.prependLeft( + node.start, + '(($ => (globalThis.__TYPEGPU_META__ ??= new WeakMap()).set($.f = (', + ).appendRight( + node.end, + `), ${metadata}) && $.f)({}))`, + ); +} + +export function wrapInAutoName( + magicString: MagicStringAST, + node: acorn.Node, + name: string, +) { + magicString + .prependLeft( + node.start, + '((globalThis.__TYPEGPU_AUTONAME__ ?? (a => a))(', + ) + .appendRight(node.end, `, "${name}"))`); +} + +export const rollUpImpl = (rawOptions: Options) => { + const options = defu(rawOptions, defaultOptions); + + return { + name: 'unplugin-typegpu' as const, + enforce: options.enforce, + transform: { + filter: options.earlyPruning + ? { + id: options, + code: earlyPruneRegex, + } + : { + id: options, + }, + handler( + this: UnpluginBuildContext & UnpluginContext, + code: string, + id: string, + ) { + const ctx: Context = { + tgpuAliases: new Set( + options.forceTgpuAlias ? [options.forceTgpuAlias] : [], + ), + fileId: id, + autoNamingEnabled: options.autoNamingEnabled, + }; + + let ast: Node; + try { + ast = this.parse(code, { + lang: 'ts', + allowReturnOutsideFunction: true, + }) as Node; + } catch (cause) { + console.warn( + `[unplugin-typegpu] Failed to parse ${id}. Cause: ${ + typeof cause === 'object' && cause && 'message' in cause + ? cause.message + : cause + }`, + ); + return undefined; + } + + const tgslFunctionDefs: { + def: FunctionNode; + name?: string | undefined; + }[] = []; + + const magicString = new MagicStringAST(code); + + walk(ast, { + enter(_node, _parent, prop, index) { + const node = _node as acorn.AnyNode; + const parent = _parent as acorn.AnyNode; + + performExpressionNaming(ctx, node, (node, name) => { + wrapInAutoName(magicString, node, name); + }); + + if (node.type === 'ImportDeclaration') { + gatherTgpuAliases(node, ctx); + } + + if (node.type === 'CallExpression') { + if (isShellImplementationCall(node, ctx)) { + const implementation = node.arguments[0]; + + if ( + implementation && + (implementation.type === 'FunctionExpression' || + implementation.type === 'ArrowFunctionExpression') + ) { + tgslFunctionDefs.push({ + def: removeUseGpuDirective(implementation), + }); + this.skip(); + } + } + } + + if ( + node.type === 'ArrowFunctionExpression' || + node.type === 'FunctionExpression' || + node.type === 'FunctionDeclaration' + ) { + if (containsUseGpuDirective(node)) { + tgslFunctionDefs.push({ + def: removeUseGpuDirective(node), + name: getFunctionName(node, parent), + }); + this.skip(); + } + } + }, + }); + + for (const { def, name } of tgslFunctionDefs) { + const { params, body, externalNames } = transpileFn(def); + const isFunctionStatement = def.type === 'FunctionDeclaration'; + + if ( + isFunctionStatement && + name && + code.slice(0, def.start) + .search(new RegExp(`(?=18'} @@ -1731,6 +1746,9 @@ packages: resolution: {integrity: sha512-sURvQbbKsq5f8INV54YJgJEdk8oxBanqkTiXXd33rKmofFCwZLhLRszPduMZ9TA9b8/1CHc/IJmOlBHJk2Q5AQ==} engines: {node: '>=18'} + '@napi-rs/wasm-runtime@1.0.3': + resolution: {integrity: sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1815,6 +1833,13 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@oxc-project/runtime@0.82.2': + resolution: {integrity: sha512-cYxcj5CPn/vo5QSpCZcYzBiLidU5+GlFSqIeNaMgBDtcVRBsBJHZg3pHw999W6nHamFQ1EHuPPByB26tjaJiJw==} + engines: {node: '>=6.9.0'} + + '@oxc-project/types@0.82.2': + resolution: {integrity: sha512-WMGSwd9FsNBs/WfqIOH0h3k1LBdjZJQGYjGnC+vla/fh6HUsu5HzGPerRljiq1hgMQ6gs031YJR12VyP57b/hQ==} + '@pagefind/darwin-arm64@1.3.0': resolution: {integrity: sha512-365BEGl6ChOsauRjyVpBjXybflXAOvoMROw3TucAROHIcdBvXk9/2AmEvGFU0r75+vdQI4LJdJdpH4Y6Yqaj4A==} cpu: [arm64] @@ -2129,9 +2154,86 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rolldown/binding-android-arm64@1.0.0-beta.33': + resolution: {integrity: sha512-xhDQXKftRkEULIxCddrKMR8y0YO/Y+6BKk/XrQP2B29YjV2wr8DByoEz+AHX9BfLHb2srfpdN46UquBW2QXWpQ==} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.33': + resolution: {integrity: sha512-7lhhY08v5ZtRq8JJQaJ49fnJombAPnqllKKCDLU/UvaqNAOEyTGC8J1WVOLC4EA4zbXO5U3CCRgVGyAFNH2VtQ==} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.33': + resolution: {integrity: sha512-U2iGjcDV7NWyYyhap8YuY0nwrLX6TvX/9i7gBtdEMPm9z3wIUVGNMVdGlA43uqg7xDpRGpEqGnxbeDgiEwYdnA==} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.33': + resolution: {integrity: sha512-gd6ASromVHFLlzrjJWMG5CXHkS7/36DEZ8HhvGt2NN8eZALCIuyEx8HMMLqvKA7z4EAztVkdToVrdxpGMsKZxw==} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.33': + resolution: {integrity: sha512-xmeLfkfGthuynO1EpCdyTVr0r4G+wqvnKCuyR6rXOet+hLrq5HNAC2XtP/jU2TB4Bc6aiLYxl868B8CGtFDhcw==} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.33': + resolution: {integrity: sha512-cHGp8yfHL4pes6uaLbO5L58ceFkUK4efd8iE86jClD1QPPDLKiqEXJCFYeuK3OfODuF5EBOmf0SlcUZNEYGdmw==} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.33': + resolution: {integrity: sha512-wZ1t7JAvVeFgskH1L9y7c47ITitPytpL0s8FmAT8pVfXcaTmS58ZyoXT+y6cz8uCkQnETjrX3YezTGI18u3ecg==} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.33': + resolution: {integrity: sha512-cDndWo3VEYbm7yeujOV6Ie2XHz0K8YX/R/vbNmMo03m1QwtBKKvbYNSyJb3B9+8igltDjd8zNM9mpiNNrq/ekQ==} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.33': + resolution: {integrity: sha512-bl7uzi6es/l6LT++NZcBpiX43ldLyKXCPwEZGY1rZJ99HQ7m1g3KxWwYCcGxtKjlb2ExVvDZicF6k+96vxOJKg==} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.33': + resolution: {integrity: sha512-TrgzQanpLgcmmzolCbYA9BPZgF1gYxkIGZhU/HROnJPsq67gcyaYw/JBLioqQLjIwMipETkn25YY799D2OZzJA==} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.33': + resolution: {integrity: sha512-z0LltdUfvoKak9SuaLz/M9AVSg+RTOZjFksbZXzC6Svl1odyW4ai21VHhZy3m2Faeeb/rl/9efVLayj+qYEGxw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.33': + resolution: {integrity: sha512-CpvOHyqDNOYx9riD4giyXQDIu72bWRU2Dwt1xFSPlBudk6NumK0OJl6Ch+LPnkp5podQHcQg0mMauAXPVKct7g==} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.33': + resolution: {integrity: sha512-/tNTvZTWHz6HiVuwpR3zR0kGIyCNb+/tFhnJmti+Aw2fAXs3l7Aj0DcXd0646eFKMX8L2w5hOW9H08FXTUkN0g==} + cpu: [ia32] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.33': + resolution: {integrity: sha512-Bb2qK3z7g2mf4zaKRvkohHzweaP1lLbaoBmXZFkY6jJWMm0Z8Pfnh8cOoRlH1IVM1Ufbo8ZZ1WXp1LbOpRMtXw==} + cpu: [x64] + os: [win32] + + '@rolldown/browser@1.0.0-beta.43': + resolution: {integrity: sha512-l00mWA7r+8mEbu04lyH1xhO5t/J6mt7yUt6VidtHqike9e8obarB6ePpzr7EA0GvsnuUYgvHhKPqRbnqSKrXJQ==} + hasBin: true + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rolldown/pluginutils@1.0.0-beta.33': + resolution: {integrity: sha512-she25NCG6NoEPC/SEB4pHs5STcnfI4VBFOzjeI63maSPrWME5J2XC8ogrBgp8NaE/xzj28/kbpSaebiMvFRj+w==} + '@rollup/plugin-alias@5.1.1': resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} engines: {node: '>=14.0.0'} @@ -2440,6 +2542,9 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@tybys/wasm-util@0.10.0': + resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} @@ -2742,6 +2847,10 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + ansis@4.1.0: + resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} + engines: {node: '>=14'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -3954,10 +4063,6 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true - jiti@2.5.1: - resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} - hasBin: true - jiti@2.6.0: resolution: {integrity: sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==} hasBin: true @@ -5242,6 +5347,10 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + rolldown@1.0.0-beta.33: + resolution: {integrity: sha512-mgu118ZuRguC8unhPCbdZbyRbjQfEMiWqlojBA5aRIncBelRaBomnHNpGKYkYWeK7twRz5Cql30xgqqrA3Xelw==} + hasBin: true + rollup-plugin-dts@6.1.1: resolution: {integrity: sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==} engines: {node: '>=16'} @@ -6857,11 +6966,24 @@ snapshots: '@emmetio/stream-reader@2.2.0': {} + '@emnapi/core@1.5.0': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + '@emnapi/runtime@1.4.3': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.8.1 + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + '@esbuild/aix-ppc64@0.24.2': optional: true @@ -7402,6 +7524,12 @@ snapshots: strict-event-emitter: 0.5.1 optional: true + '@napi-rs/wasm-runtime@1.0.3': + dependencies: + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 + '@tybys/wasm-util': 0.10.0 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7506,6 +7634,10 @@ snapshots: '@oslojs/encoding@1.1.0': {} + '@oxc-project/runtime@0.82.2': {} + + '@oxc-project/types@0.82.2': {} + '@pagefind/darwin-arm64@1.3.0': optional: true @@ -7778,8 +7910,58 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@rolldown/binding-android-arm64@1.0.0-beta.33': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.33': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.33': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.33': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.33': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.33': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.33': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.33': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.33': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.33': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.33': + dependencies: + '@napi-rs/wasm-runtime': 1.0.3 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.33': + optional: true + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.33': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.33': + optional: true + + '@rolldown/browser@1.0.0-beta.43': + dependencies: + '@napi-rs/wasm-runtime': 1.0.3 + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rolldown/pluginutils@1.0.0-beta.33': {} + '@rollup/plugin-alias@5.1.1(rollup@4.34.8)': optionalDependencies: rollup: 4.34.8 @@ -7953,7 +8135,7 @@ snapshots: dependencies: '@jridgewell/remapping': 2.3.5 enhanced-resolve: 5.18.3 - jiti: 2.5.1 + jiti: 2.6.0 lightningcss: 1.30.1 magic-string: 0.30.19 source-map-js: 1.2.1 @@ -8037,6 +8219,10 @@ snapshots: '@trysound/sax@0.2.0': {} + '@tybys/wasm-util@0.10.0': + dependencies: + tslib: 2.8.1 + '@types/acorn@4.0.6': dependencies: '@types/estree': 1.0.8 @@ -8391,6 +8577,8 @@ snapshots: ansi-styles@6.2.3: {} + ansis@4.1.0: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -9915,8 +10103,6 @@ snapshots: jiti@1.21.7: {} - jiti@2.5.1: {} - jiti@2.6.0: {} jotai-location@0.6.2(jotai@2.15.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.1.8)(react@19.1.0)): @@ -11544,6 +11730,28 @@ snapshots: robust-predicates@3.0.2: {} + rolldown@1.0.0-beta.33: + dependencies: + '@oxc-project/runtime': 0.82.2 + '@oxc-project/types': 0.82.2 + '@rolldown/pluginutils': 1.0.0-beta.33 + ansis: 4.1.0 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.33 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.33 + '@rolldown/binding-darwin-x64': 1.0.0-beta.33 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.33 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.33 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.33 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.33 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.33 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.33 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.33 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.33 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.33 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.33 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.33 + rollup-plugin-dts@6.1.1(rollup@4.34.8)(typescript@5.8.3): dependencies: magic-string: 0.30.19 @@ -12068,7 +12276,7 @@ snapshots: esbuild: 0.25.10 fix-dts-default-cjs-exports: 1.0.0 hookable: 5.5.3 - jiti: 2.5.1 + jiti: 2.6.0 magic-string: 0.30.19 mkdist: 2.2.0(typescript@5.8.3) mlly: 1.7.4 @@ -12195,7 +12403,7 @@ snapshots: dependencies: citty: 0.1.6 defu: 6.1.4 - jiti: 2.5.1 + jiti: 2.6.0 knitwork: 1.2.0 scule: 1.3.0