diff --git a/packages/typegpu-cli/src/create.ts b/packages/typegpu-cli/src/create.ts index 492b9ceac5..305ceb4227 100644 --- a/packages/typegpu-cli/src/create.ts +++ b/packages/typegpu-cli/src/create.ts @@ -3,7 +3,7 @@ import * as p from '@clack/prompts'; import { pmFromUserAgent, pmInstall } from './utils/pm.ts'; import { cancelExit, confirmStep, rgbText } from './utils/prompts.ts'; -import { copyTemplate, prepareDirectory } from './utils/files.ts'; +import { scaffoldProject, prepareDirectory } from './utils/files.ts'; import { getPackageName, getProjectDirectory } from './utils/inputs.ts'; import { detect, resolveCommand } from 'package-manager-detector'; @@ -40,7 +40,7 @@ export async function createProject(cwd: string) { '../templates', `template-${projectTemplate}`, ); - copyTemplate(templateDir, root, packageName); + await scaffoldProject(templateDir, root, packageName); p.log.success(`Scaffolded project at ${projectDir}.`); diff --git a/packages/typegpu-cli/src/enhance.ts b/packages/typegpu-cli/src/enhance.ts index 2cae382a21..bd2c2e36a1 100644 --- a/packages/typegpu-cli/src/enhance.ts +++ b/packages/typegpu-cli/src/enhance.ts @@ -4,19 +4,22 @@ import * as p from '@clack/prompts'; import { detect, type Agent } from 'package-manager-detector'; import { type } from 'arktype'; -import { PackageJsonWithDepsSchema, type PackageJsonWithDeps } from './utils/types.ts'; +import { PackageJsonSchema, type PackageJson } from './utils/types.ts'; import { pmInstall } from './utils/pm.ts'; import { cancelExit, confirmStep, failAndExit, rgbText } from './utils/prompts.ts'; -import { ensureWebgpuTypes } from './steps/webgpu-types.ts'; -import { ensureTypegpu } from './steps/typegpu.ts'; -import { ensureVite } from './steps/vite.ts'; +import { askForWebgpuTypes } from './steps/webgpu-types.ts'; +import { askForPkgs, ensureTypegpu } from './steps/typegpu.ts'; +import { askForVite } from './steps/vite.ts'; const PROJECT_KINDS = [{ value: 'vite', label: rgbText('Vite', 175, 105, 245) }]; -async function runViteFlow(cwd: string, pm: Agent, pkg: PackageJsonWithDeps) { - await ensureWebgpuTypes(cwd, pm, pkg); - await ensureVite(cwd, pm, pkg); - await ensureTypegpu(pm, pkg); +async function runViteFlow(cwd: string, pm: Agent, pkg: PackageJson) { + await askForWebgpuTypes(cwd, pm, pkg); + await askForVite(cwd, pm, pkg); + if (!(await ensureTypegpu(pm, pkg))) { + return; + } + await askForPkgs(pm, pkg); } export async function enhanceProject(cwd: string) { @@ -35,7 +38,7 @@ export async function enhanceProject(cwd: string) { } // normal JSON.parse is fine because package.json cannot contain comments - const pkg = PackageJsonWithDepsSchema(JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))); + const pkg = PackageJsonSchema(JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))); if (pkg instanceof type.errors) { failAndExit('Could not parse package.json.', pkg.summary); } diff --git a/packages/typegpu-cli/src/steps/typegpu.ts b/packages/typegpu-cli/src/steps/typegpu.ts index c61d2efb5a..a06a37581f 100644 --- a/packages/typegpu-cli/src/steps/typegpu.ts +++ b/packages/typegpu-cli/src/steps/typegpu.ts @@ -1,17 +1,26 @@ import type { Agent } from 'package-manager-detector'; import * as p from '@clack/prompts'; -import { hasDependency } from '../utils/pkg.ts'; -import type { PackageJsonWithDeps } from '../utils/types.ts'; +import { appendVersion, hasDependency } from '../utils/pkg.ts'; +import type { PackageJson } from '../utils/types.ts'; import { pmAdd } from '../utils/pm.ts'; import { confirmStep } from '../utils/prompts.ts'; +import { multiselectPkgs } from '../utils/inputs.ts'; -export async function ensureTypegpu(pm: Agent, pkg: PackageJsonWithDeps) { +export async function ensureTypegpu(pm: Agent, pkg: PackageJson): Promise { if (hasDependency(pkg, 'typegpu')) { p.log.info('typegpu is already installed.'); - return; + return true; } - if (!(await confirmStep('Install typegpu?'))) return; + if (!(await confirmStep('Install typegpu?'))) return false; pmAdd(pm, ['typegpu'], false); // no p.log.success because pmAdd already logs it + return true; +} + +export async function askForPkgs(pm: Agent, pkg: PackageJson) { + const pkgs = (await multiselectPkgs(pkg))?.map(({ pkg, ver }) => appendVersion(pkg, ver)); + if (pkgs) { + pmAdd(pm, pkgs, false); + } } diff --git a/packages/typegpu-cli/src/steps/vite.ts b/packages/typegpu-cli/src/steps/vite.ts index a578c987ba..c478c26859 100644 --- a/packages/typegpu-cli/src/steps/vite.ts +++ b/packages/typegpu-cli/src/steps/vite.ts @@ -9,7 +9,7 @@ import { findConfig } from '../utils/config.ts'; import { hasDependency } from '../utils/pkg.ts'; import { pmAdd } from '../utils/pm.ts'; import { confirmStep, failAndExit } from '../utils/prompts.ts'; -import type { PackageJsonWithDeps } from '../utils/types.ts'; +import type { PackageJson } from '../utils/types.ts'; const VITE_CONFIG_NAMES = [ 'vite.config.ts', @@ -46,7 +46,7 @@ function createViteConfig(cwd: string) { p.log.success('Created vite.config.ts.'); } -export async function ensureVite(cwd: string, pm: Agent, pkg: PackageJsonWithDeps) { +export async function askForVite(cwd: string, pm: Agent, pkg: PackageJson) { if (hasDependency(pkg, 'unplugin-typegpu')) { p.log.info('unplugin-typegpu is already installed.'); return; diff --git a/packages/typegpu-cli/src/steps/webgpu-types.ts b/packages/typegpu-cli/src/steps/webgpu-types.ts index 17aa763afe..d3dfcb9a28 100644 --- a/packages/typegpu-cli/src/steps/webgpu-types.ts +++ b/packages/typegpu-cli/src/steps/webgpu-types.ts @@ -9,7 +9,7 @@ import { hasDependency } from '../utils/pkg.ts'; import { findConfig } from '../utils/config.ts'; import { pmAdd } from '../utils/pm.ts'; import { confirmStep, failAndExit } from '../utils/prompts.ts'; -import { TsConfigSchema, type PackageJsonWithDeps } from '../utils/types.ts'; +import { TsConfigSchema, type PackageJson } from '../utils/types.ts'; const TS_CONFIG_NAMES = ['tsconfig.app.json', 'tsconfig.json']; @@ -40,7 +40,7 @@ function addWebgpuTypesToTsconfig(filePath: string) { fs.writeFileSync(filePath, stringify(tsconfig, null, 2) + '\n'); } -export async function ensureWebgpuTypes(cwd: string, pm: Agent, pkg: PackageJsonWithDeps) { +export async function askForWebgpuTypes(cwd: string, pm: Agent, pkg: PackageJson) { if (hasDependency(pkg, '@webgpu/types')) { p.log.info('@webgpu/types package is already installed.'); return; diff --git a/packages/typegpu-cli/src/utils/files.ts b/packages/typegpu-cli/src/utils/files.ts index 506d67cbbc..5255efa996 100644 --- a/packages/typegpu-cli/src/utils/files.ts +++ b/packages/typegpu-cli/src/utils/files.ts @@ -3,8 +3,9 @@ import path from 'node:path'; import * as p from '@clack/prompts'; import { cancelExit, failAndExit } from './prompts.ts'; -import { PackageJsonWithNameSchema } from './types.ts'; import { type } from 'arktype'; +import { PackageJsonSchema } from './types.ts'; +import { multiselectPkgs } from './inputs.ts'; const renameFiles = { _gitignore: '.gitignore', @@ -49,7 +50,11 @@ export async function prepareDirectory(cwd: string, projectDir: string) { return dir; } -export function copyTemplate(templateDir: string, projectDir: string, packageName: string) { +export async function scaffoldProject( + templateDir: string, + projectDir: string, + packageName: string, +) { const entries = fs.readdirSync(templateDir); for (const entry of entries.filter((f) => f !== '_package.json' && f !== 'index.html')) { const src = path.join(templateDir, entry); @@ -65,10 +70,18 @@ export function copyTemplate(templateDir: string, projectDir: string, packageNam const srcPackage = path.join(templateDir, '_package.json'); const destPackage = path.join(projectDir, 'package.json'); - const pkg = PackageJsonWithNameSchema(JSON.parse(fs.readFileSync(srcPackage, 'utf-8'))); + const pkg = PackageJsonSchema(JSON.parse(fs.readFileSync(srcPackage, 'utf-8'))); if (pkg instanceof type.errors) { failAndExit(`[INTERNAL] Invalid package.json in template ${templateDir}`, pkg.summary); } pkg.name = packageName; + const pkgs = await multiselectPkgs(pkg); + if (pkgs) { + pkg.dependencies ??= {}; + for (const { pkg: dep, ver } of pkgs) { + pkg.dependencies[dep] = ver; + } + } + fs.writeFileSync(destPackage, JSON.stringify(pkg, null, 2) + '\n' /* to make oxfmt happy */); } diff --git a/packages/typegpu-cli/src/utils/inputs.ts b/packages/typegpu-cli/src/utils/inputs.ts index dd3b263561..cea14ece6c 100644 --- a/packages/typegpu-cli/src/utils/inputs.ts +++ b/packages/typegpu-cli/src/utils/inputs.ts @@ -1,5 +1,7 @@ import * as p from '@clack/prompts'; import { cancelExit } from './prompts.ts'; +import type { PackageJson } from './types.ts'; +import { hasDependency, typegpuPkgs, VERSION } from './pkg.ts'; function isValidProjectDirectory(projectDir: string) { return !/[<>:"\\|?*\s]|\/+$/.test(projectDir.trim()); @@ -43,3 +45,23 @@ export async function getPackageName(initialValue: string) { return packageName.trim(); } + +export async function multiselectPkgs(pkg: PackageJson) { + const options = typegpuPkgs.filter((entry) => !hasDependency(pkg, entry.value)); + if (options.length === 0) { + p.log.info('All typegpu ecosystem packages are already installed.'); + return; + } + + const packages = await p.multiselect({ + message: "Pick packages to add ('space' to select, 'enter' to confirm):", + options: options, + required: false, + }); + + if (p.isCancel(packages)) { + cancelExit(); + } + + return packages.map((pkgName) => ({ pkg: pkgName, ver: VERSION })); +} diff --git a/packages/typegpu-cli/src/utils/pkg.ts b/packages/typegpu-cli/src/utils/pkg.ts index 342f0e5372..ad9911b1f6 100644 --- a/packages/typegpu-cli/src/utils/pkg.ts +++ b/packages/typegpu-cli/src/utils/pkg.ts @@ -1,8 +1,29 @@ -import type { PackageJsonWithDeps } from './types.ts'; +import type { PackageJson } from './types.ts'; -export function hasDependency(pkg: PackageJsonWithDeps, name: string) { +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 const VERSION = '^0.11.0'; + +export const typegpuPkgs = [ + { value: '@typegpu/color', hint: 'helpers for converting color spaces' }, + // { value: '@typegpu/geometry', hint: 'helpers for drawing points and lines' }, + // { value: '@typegpu/gl', hint: 'WebGL utilities for TypeGPU integration' }, + { + value: '@typegpu/noise', + hint: 'helpers for Perlin noise and general purpose random number generation.', + }, + { value: '@typegpu/radiance-cascades', hint: 'implementation of Radiance Cascades algorithm' }, + { value: '@typegpu/react', hint: 'React hooks for TypeGPU integration' }, + { value: '@typegpu/sdf', hint: 'helpers for creating Signed Distance Fields' }, + // { value: '@typegpu/sort', hint: 'implementations of scanning and sorting algorithms' }, + { value: '@typegpu/three', hint: 'Three.js utilities for TypeGPU integration' }, +] as const satisfies { value: string; hint: string }[]; + +export function appendVersion(pkg: string, version: string) { + return `${pkg}@${version}`; +} diff --git a/packages/typegpu-cli/src/utils/pm.ts b/packages/typegpu-cli/src/utils/pm.ts index a3191f8a70..240d98dee5 100644 --- a/packages/typegpu-cli/src/utils/pm.ts +++ b/packages/typegpu-cli/src/utils/pm.ts @@ -38,6 +38,11 @@ function runCommand(command: string, args: string[], interactive?: boolean) { } export function pmAdd(pm: Agent, pkgs: string[], dev: boolean) { + if (pkgs.length === 0) { + p.log.success('No packages to install.'); + return; + } + const args = dev ? ['-D', ...pkgs] : pkgs; const cmd = resolveCommand(pm, 'add', args); if (!cmd) { diff --git a/packages/typegpu-cli/src/utils/types.ts b/packages/typegpu-cli/src/utils/types.ts index ff0033e962..c0b349aecf 100644 --- a/packages/typegpu-cli/src/utils/types.ts +++ b/packages/typegpu-cli/src/utils/types.ts @@ -1,16 +1,12 @@ import { type } from 'arktype'; -export const PackageJsonWithDepsSchema = type({ +export const PackageJsonSchema = type({ + name: 'string', 'dependencies?': 'Record', 'devDependencies?': 'Record', 'peerDependencies?': 'Record', }); -export type PackageJsonWithDeps = typeof PackageJsonWithDepsSchema.infer; - -export const PackageJsonWithNameSchema = type({ - name: 'string', -}); -export type PackageJsonWithName = typeof PackageJsonWithNameSchema.infer; +export type PackageJson = typeof PackageJsonSchema.infer; export const TsConfigSchema = type({ 'compilerOptions?': {