Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
1 change: 1 addition & 0 deletions packages/tgpu/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test-projects
Comment thread
cieplypolar marked this conversation as resolved.
Outdated
23 changes: 23 additions & 0 deletions packages/tgpu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# tgpu
Enhance your project with TypeGPU.

## Usage
Run inside the root of an existing Vite or React Native project:

```sh
pnpm dlx tgpu
# or
npx tgpu
# or
yarn dlx tgpu
# or
bunx tgpu
```
Comment thread
cieplypolar marked this conversation as resolved.

> [!CAUTION]
> **Back up your project before running this tool.** Even though every modification is gated behind an explicit "yes/no" prompt, the changes the script makes are not reversible by it.
---

TODO:
- [ ] when we have templates, remember to add CC0 License
- check [vite](https://github.com/vitejs/vite/blob/main/packages/create-vite/tsdown.config.ts)
Comment thread
cieplypolar marked this conversation as resolved.
Outdated
45 changes: 45 additions & 0 deletions packages/tgpu/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "tgpu",
"version": "0.0.1",
"description": "Add TypeGPU to your project",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/software-mansion/TypeGPU.git#main",
"directory": "packages/tgpu"
},
"bin": {
"tgpu": "./dist/index.js"
},
"files": [
"dist"
],
"type": "module",
"scripts": {
"dev": "tsdown && node ./dist/index.js",
"build": "tsdown",
"test:types": "pnpm tsc --p ./tsconfig.json --noEmit",
"prepublishOnly": "pnpm run build",
"test:dummy": "tsdown && cd src/test-projects/vite && node ../../../dist/index.js"
Comment thread
cieplypolar marked this conversation as resolved.
Outdated
},
"dependencies": {
"@antfu/ni": "^25.0.0",
"@clack/prompts": "^0.9.1",
"comment-json": "^5.0.0",
"cross-spawn": "^7.0.6",
"magicast": "^0.5.2",
"mri": "^1.2.0",
"package-manager-detector": "^1.3.0"
},
"devDependencies": {
"@types/cross-spawn": "^6.0.6",
"@types/node": "catalog:types",
"arktype": "catalog:",
"tsdown": "catalog:build",
"typescript": "catalog:types"
},
"engines": {
"node": ">=24.0.0"
},
"packageManager": "pnpm@10.15.1"
}
94 changes: 94 additions & 0 deletions packages/tgpu/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env node

import fs from 'node:fs';
import path from 'node:path';
import mri from 'mri';
import * as p from '@clack/prompts';
import { detect, type Agent } from 'package-manager-detector';
import { type } from 'arktype';

import { PackageJsonSchema, type PackageJson } from './utils/types.ts';
import { pmInstall } from './utils/pm.ts';

import { cancelExit, failAndExit } from './utils/prompts.ts';
import { ensureWebgpuTypes } from './steps/webgpu-types.ts';
import { ensureTypegpu } from './steps/typegpu.ts';
import { ensureVite } from './steps/vite.ts';

async function runViteFlow(cwd: string, pm: Agent, pkg: PackageJson): Promise<void> {
await ensureWebgpuTypes(cwd, pm, pkg);
await ensureVite(cwd, pm, pkg);
await ensureTypegpu(pm, pkg);
}

// async function runReactNativeFlow(cwd: string, pm: Agent, pkg: PackageJson | null): Promise<void> {
// const isExpo = isExpoProject(pkg);

// await ensureWebgpuTypes(cwd, pm, pkg);
// await ensureReactNativeWgpu(pm, pkg);
// await ensureBabel(cwd, pm, pkg);

// if (isExpo) await expoCustomize(cwd, pm);

// await ensureTypegpu(pm, pkg);

// if (isExpo) {
// await expoClearCache(cwd, pm);
// await expoPrebuild(cwd, pm);
// await podInstall(cwd);
// }
// }

// real script starts here
const argv = mri(process.argv.slice(2), {
alias: { h: 'help' },
boolean: ['help'],
});

if (argv.help) {
console.log('Usage: node tgpu'); // TODO: change this after publish
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only after publish?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know the final command at that time 🙈

process.exit(0);
}

p.intro('Enhancing project with TypeGPU');

const cwd = process.cwd();
const pm = await detect({ cwd });
if (!pm) {
failAndExit('Could not detect package manager.');
}
const pmAgent = pm.agent;
p.log.info(`Detected package manager: ${pmAgent}`);

const pkgPath = path.join(cwd, 'package.json');
if (!fs.existsSync(pkgPath)) {
failAndExit('No package.json found in the current directory.');
}
const pkg = PackageJsonSchema(JSON.parse(fs.readFileSync(pkgPath, 'utf-8')));
if (pkg instanceof type.errors) {
failAndExit('Could not parse package.json', pkg.summary);
}

const projectKind = await p.select({
message: 'What kind of project is this?',
options: [
{ value: 'vite', label: 'Vite' },
{ value: 'react-native', label: 'React Native' },
],
});
if (p.isCancel(projectKind)) {
cancelExit();
}

switch (projectKind) {
case 'vite':
await runViteFlow(cwd, pm.agent, pkg);
break;
case 'react-native':
// await runReactNativeFlow(cwd, pm.agent, pkg);
break;
default:
failAndExit('Unsupported project kind.');
}
await pmInstall(pm.agent);
p.outro('Done! Get ready for shaderful experience.');
Comment thread
cieplypolar marked this conversation as resolved.
Outdated
16 changes: 16 additions & 0 deletions packages/tgpu/src/steps/typegpu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Agent } from 'package-manager-detector';
import * as p from '@clack/prompts';

import { hasDependency } from '../utils/pkg.ts';
import type { PackageJson } from '../utils/types.ts';
import { pmAdd } from '../utils/pm.ts';
import { confirmStep } from '../utils/prompts.ts';

export async function ensureTypegpu(pm: Agent, pkg: PackageJson) {
if (hasDependency(pkg, 'typegpu')) {
p.log.info('typegpu is already installed.');
return;
}
if (!(await confirmStep('Install typegpu?'))) return;
await pmAdd(pm, ['typegpu'], false);
}
68 changes: 68 additions & 0 deletions packages/tgpu/src/steps/vite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import fs from 'node:fs';
import path from 'node:path';
import * as p from '@clack/prompts';
import type { Agent } from 'package-manager-detector';
import { loadFile, writeFile } from 'magicast';
import { addVitePlugin } from 'magicast/helpers';

import { hasDependency } from '../utils/pkg.ts';
import { pmAdd } from '../utils/pm.ts';
import { confirmStep } from '../utils/prompts.ts';
import type { PackageJson } from '../utils/types.ts';

const VITE_CONFIG_NAMES = [
'vite.config.ts',
'vite.config.js',
'vite.config.mts',
'vite.config.mjs',
'vite.config.cts',
'vite.config.cjs',
];

const TEMPLATE = `import { defineConfig } from 'vite';
import typegpuPlugin from 'unplugin-typegpu/vite';

export default defineConfig({
plugins: [typegpuPlugin()],
});
`;

function findViteConfig(cwd: string) {
for (const name of VITE_CONFIG_NAMES) {
const viteConfigPath = path.join(cwd, name);
if (fs.existsSync(viteConfigPath)) return viteConfigPath;
}
return undefined;
}

async function setupViteConfig(filePath: string) {
const config = await loadFile(filePath);
addVitePlugin(config, { from: 'unplugin-typegpu/vite', constructor: 'typegpuPlugin' });
await writeFile(config, filePath);
p.log.success(`Updated ${path.basename(filePath)}.`);
}

function createViteConfig(cwd: string) {
fs.writeFileSync(path.join(cwd, 'vite.config.ts'), TEMPLATE);
p.log.success('Created vite.config.ts.');
}

export async function ensureVite(cwd: string, pm: Agent, pkg: PackageJson) {
if (hasDependency(pkg, 'unplugin-typegpu')) {
p.log.info('unplugin-typegpu is already installed.');
return;
}

if (!(await confirmStep('Install unplugin-typegpu and configure vite?'))) {
return;
}

await pmAdd(pm, ['unplugin-typegpu'], true);

const viteConfigPath = findViteConfig(cwd);
if (viteConfigPath) {
await setupViteConfig(viteConfigPath);
} else if (await confirmStep('No vite config found. Create vite.config.ts?')) {
createViteConfig(cwd);
}
}
67 changes: 67 additions & 0 deletions packages/tgpu/src/steps/webgpu-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import fs from 'node:fs';
import path from 'node:path';
import * as p from '@clack/prompts';
import { parse, stringify } from 'comment-json';

import { hasDependency } from '../utils/pkg.ts';
import { pmAdd } from '../utils/pm.ts';
import { confirmStep, failAndExit } from '../utils/prompts.ts';
import { TsConfigSchema, type PackageJson } from '../utils/types.ts';
import type { Agent } from 'package-manager-detector';
import { type } from 'arktype';

function findTsconfig(cwd: string) {
for (const name of ['tsconfig.app.json', 'tsconfig.json']) {
const full = path.join(cwd, name);
if (fs.existsSync(full)) return full;
}
return undefined;
}

function addWebgpuTypesToTsconfig(filePath: string) {
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parse(content);
if (!parsed) {
failAndExit(`Could not parse tsconfig.`);
}
const tsconfig = TsConfigSchema(parsed);
if (tsconfig instanceof type.errors) {
failAndExit(`Could not parse tsconfig.`, tsconfig.summary);
}

if (!tsconfig.compilerOptions) {
tsconfig.compilerOptions = {};
}

if (!tsconfig.compilerOptions.types) {
tsconfig.compilerOptions.types = [];
}

if (tsconfig.compilerOptions.types?.includes('@webgpu/types')) {
return;
}

tsconfig.compilerOptions.types.push('@webgpu/types');
fs.writeFileSync(filePath, stringify(tsconfig, null, 2) + '\n');
}

export async function ensureWebgpuTypes(cwd: string, pm: Agent, pkg: PackageJson) {
if (hasDependency(pkg, '@webgpu/types')) {
p.log.info('@webgpu/types is already installed.');
return;
}

if (!(await confirmStep('Install @webgpu/types and add to tsconfig?'))) {
return;
}

const tsconfig = findTsconfig(cwd);
if (!tsconfig) {
failAndExit('No tsconfig found, cannot register @webgpu/types.');
}

await pmAdd(pm, ['@webgpu/types'], true);

addWebgpuTypesToTsconfig(tsconfig);
p.log.success(`Added @webgpu/types to ${path.basename(tsconfig)}.`);
}
12 changes: 12 additions & 0 deletions packages/tgpu/src/utils/pkg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { PackageJson } from './types.ts';

export function hasDependency(pkg: PackageJson, name: string) {
const deps = pkg.dependencies ?? {};
const devDeps = pkg.devDependencies ?? {};
const peerDeps = pkg.peerDependencies ?? {};
return name in deps || name in devDeps || name in peerDeps;
}

export function isExpoProject(pkg: PackageJson) {
return hasDependency(pkg, 'expo');
}
48 changes: 48 additions & 0 deletions packages/tgpu/src/utils/pm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import spawn from 'cross-spawn';
import * as p from '@clack/prompts';
import { resolveCommand } from '@antfu/ni';
import type { Agent } from 'package-manager-detector';
import { failAndExit } from './prompts.ts';

async function runCommand(command: string, args: string[]) {
const child = spawn(command, args, { stdio: ['inherit', 'ignore', 'inherit'] });

return await new Promise((resolve, reject) => {
child.on('error', reject);
child.on('close', (code) => resolve(code ?? 1));
});
}

export async function pmAdd(pm: Agent, pkgs: string[], dev: boolean) {
const args = dev ? ['-D', ...pkgs] : pkgs;
const cmd = resolveCommand(pm, 'add', args);
if (!cmd) {
failAndExit(`Cannot resolve add command for ${pm}.`);
}

const label = pkgs.join(', ');
const s = p.spinner();
s.start(`Installing ${label}`);
const status = await runCommand(cmd.command, cmd.args);
if (status !== 0) {
s.stop(`Failed to install ${label}.`, 1);
failAndExit('Package installation failed.');
}
s.stop(`Installed ${label}`);
}

export async function pmInstall(pm: Agent) {
const cmd = resolveCommand(pm, 'install', []);
if (!cmd) {
failAndExit(`Cannot resolve install command for ${pm}.`);
}

const s = p.spinner();
s.start('Installing dependencies');
const status = await runCommand(cmd.command, cmd.args);
if (status !== 0) {
s.stop('Failed to install dependencies.', 1);
failAndExit('Dependency installation failed.');
}
s.stop('Installed dependencies');
}
Loading
Loading