From 8c5fa7fc2d19f9eebe9a19e5985b73e58844405b Mon Sep 17 00:00:00 2001 From: Gleb Stepanov Date: Thu, 9 Oct 2025 12:13:11 +0200 Subject: [PATCH 01/10] Starting to create CLI tool --- packages/cli/package.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 packages/cli/package.json diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..202abd9 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,15 @@ +{ + "name": "@glstep/create-app", + "type": "module", + "version": "0.1.0", + "description": "CLI tool to scaffold Vue/TS library projects", + "author": "Gleb Stepanov ", + "license": "MIT", + "bin": "./bin/create-app.js", + "files": ["bin", "dist", "templates"], + "scripts": { + "build": "tsup src/index.ts --format esm --dts", + "dev": "tsup src/index.ts --format esm --watch", + "typecheck": "tsc --noEmit" + } +} From 918d09e56f1412dbe73fbf5f5b4f1ddf72350e06 Mon Sep 17 00:00:00 2001 From: Gleb Stepanov Date: Thu, 9 Oct 2025 22:57:12 +0200 Subject: [PATCH 02/10] current state of application --- package.json | 2 +- packages/cli/bin/create-app.mjs | 14 +++ packages/cli/package.json | 46 +++++--- packages/cli/src/__tests__/index.spec.ts | 15 +++ packages/cli/src/index.ts | 7 ++ packages/cli/src/prompts/project.ts | 31 +++++ packages/cli/tsconfig.app.json | 17 +++ packages/cli/tsup.config.ts | 8 ++ pnpm-lock.yaml | 138 +++++++++++++++++++++++ tsconfig.json | 4 + 10 files changed, 266 insertions(+), 16 deletions(-) create mode 100644 packages/cli/bin/create-app.mjs create mode 100644 packages/cli/src/__tests__/index.spec.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/src/prompts/project.ts create mode 100644 packages/cli/tsconfig.app.json create mode 100644 packages/cli/tsup.config.ts diff --git a/package.json b/package.json index eb5d989..6d1a09c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "version": "0.1.0", "private": true, - "packageManager": "pnpm@10.18.0", + "packageManager": "pnpm@10.18.1", "description": "A monorepo template for developing Vue libraries", "author": "Gleb Stepanov ", "license": "MIT", diff --git a/packages/cli/bin/create-app.mjs b/packages/cli/bin/create-app.mjs new file mode 100644 index 0000000..80a5aa3 --- /dev/null +++ b/packages/cli/bin/create-app.mjs @@ -0,0 +1,14 @@ +#!/usr/bin/env node + +import { dirname, resolve } from 'node:path' +import process from 'node:process' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const cliPath = resolve(__dirname, '../dist/index.js') +import(pathToFileURL(cliPath).href).catch((err) => { + console.error('Failed to load CLI', err) + process.exit(1) +}) diff --git a/packages/cli/package.json b/packages/cli/package.json index 202abd9..5283e22 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,15 +1,31 @@ -{ - "name": "@glstep/create-app", - "type": "module", - "version": "0.1.0", - "description": "CLI tool to scaffold Vue/TS library projects", - "author": "Gleb Stepanov ", - "license": "MIT", - "bin": "./bin/create-app.js", - "files": ["bin", "dist", "templates"], - "scripts": { - "build": "tsup src/index.ts --format esm --dts", - "dev": "tsup src/index.ts --format esm --watch", - "typecheck": "tsc --noEmit" - } -} +{ + "name": "@glstep/vue-ts-app-lib", + "type": "module", + "version": "0.1.0", + "description": "CLI tool to scaffold Vue/TS library projects", + "author": "Gleb Stepanov ", + "license": "MIT", + "bin": { + "create-app": "./bin/create-app.mjs" + }, + "files": [ + "bin", + "dist", + "templates" + ], + "scripts": { + "build": "tsup src/index.ts --format esm --dts", + "dev": "tsup src/index.ts --format esm --watch", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "chalk": "^5.6.2", + "prompts": "^2.4.2" + }, + "devDependencies": { + "@types/prompts": "^2.4.9", + "tsup": "^8.5.0", + "typescript": "^5.9.0", + "vitest": "^3.2.4" + } +} diff --git a/packages/cli/src/__tests__/index.spec.ts b/packages/cli/src/__tests__/index.spec.ts new file mode 100644 index 0000000..129ef7d --- /dev/null +++ b/packages/cli/src/__tests__/index.spec.ts @@ -0,0 +1,15 @@ +import { execSync } from 'node:child_process' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +describe('cli', () => { + it('should output message', () => { + const binPath = resolve(__dirname, '../../bin/create-app.mjs') + + const output = execSync(`node ${binPath}`, { + encoding: 'utf-8', + }) + + expect(output).toContain('Hello, CLI!') + }) +}) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000..e6b1480 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +import { promptProjectOptions } from './prompts/project' + +console.log('Hello, CLI!') +const val = await promptProjectOptions('lib') +console.log(val) diff --git a/packages/cli/src/prompts/project.ts b/packages/cli/src/prompts/project.ts new file mode 100644 index 0000000..3a45eb0 --- /dev/null +++ b/packages/cli/src/prompts/project.ts @@ -0,0 +1,31 @@ +import chalk from 'chalk' +import prompts from 'prompts' + +export interface ProjectOptions { + projectName: string + packageName: string + scope: string + includeLib: boolean + includeLibTs: boolean + includePlayground: boolean + includeDocs: boolean + installDeps: boolean +} + +export async function promptProjectOptions(targetDir: string): Promise { + console.log(`\n${chalk.bold.cyan('Create a Vue/TS Library project')}\n`) + + const response = await prompts( + [ + { + type: 'text', + name: 'projectName', + message: 'Project name: ', + initial: targetDir === '.' ? 'my-lib' : targetDir, + validate: (value: string) => value.trim() ? true : 'Project name cannot be empty', + }, + ], + ) + + return response as ProjectOptions | null +} diff --git a/packages/cli/tsconfig.app.json b/packages/cli/tsconfig.app.json new file mode 100644 index 0000000..0bcb658 --- /dev/null +++ b/packages/cli/tsconfig.app.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "target": "ES2022", + "rootDir": "src", + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": false, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": false, + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.spec.ts"] +} diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts new file mode 100644 index 0000000..f7860df --- /dev/null +++ b/packages/cli/tsup.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + outDir: 'dist', + clean: true, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff38d96..ddc6129 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,28 @@ importers: specifier: ^3.0.0 version: 3.1.0(typescript@5.9.3) + packages/cli: + dependencies: + chalk: + specifier: ^5.6.2 + version: 5.6.2 + prompts: + specifier: ^2.4.2 + version: 2.4.2 + devDependencies: + '@types/prompts': + specifier: ^2.4.9 + version: 2.4.9 + tsup: + specifier: ^8.5.0 + version: 8.5.0(jiti@1.21.7)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.6.2)(jiti@1.21.7)(jsdom@27.0.0(postcss@8.5.6))(yaml@2.8.1) + packages/docs: dependencies: '@glstep/lib': @@ -1081,6 +1103,9 @@ packages: '@types/node@24.6.2': resolution: {integrity: sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==} + '@types/prompts@2.4.9': + resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1469,6 +1494,12 @@ packages: peerDependencies: esbuild: '>=0.17' + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1499,6 +1530,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} @@ -1516,6 +1551,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + ci-info@4.3.1: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} @@ -1570,6 +1609,10 @@ packages: config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + core-js-compat@3.45.1: resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==} @@ -1996,6 +2039,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2249,6 +2295,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -2750,6 +2800,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -2773,6 +2827,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -3099,6 +3157,25 @@ packages: typescript: optional: true + tsup@8.5.0: + resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4189,6 +4266,11 @@ snapshots: dependencies: undici-types: 7.13.0 + '@types/prompts@2.4.9': + dependencies: + '@types/node': 24.6.2 + kleur: 3.0.3 + '@types/unist@3.0.3': {} '@types/web-bluetooth@0.0.20': {} @@ -4627,6 +4709,11 @@ snapshots: esbuild: 0.17.19 load-tsconfig: 0.2.5 + bundle-require@5.1.0(esbuild@0.25.10): + dependencies: + esbuild: 0.25.10 + load-tsconfig: 0.2.5 + cac@6.7.14: {} callsites@3.1.0: {} @@ -4660,6 +4747,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + change-case@5.4.4: {} character-entities@2.0.2: {} @@ -4682,6 +4771,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + ci-info@4.3.1: {} clean-regexp@1.0.0: @@ -4724,6 +4817,8 @@ snapshots: ini: 1.3.8 proto-list: 1.2.4 + consola@3.4.2: {} + core-js-compat@3.45.1: dependencies: browserslist: 4.26.3 @@ -5273,6 +5368,12 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.19 + mlly: 1.8.0 + rollup: 4.52.4 + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -5508,6 +5609,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kleur@3.0.3: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -6142,6 +6245,11 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + proto-list@1.2.4: {} punycode@2.3.1: {} @@ -6160,6 +6268,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.1.2: {} + refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.1 @@ -6500,6 +6610,34 @@ snapshots: - supports-color - ts-node + tsup@8.5.0(jiti@1.21.7)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.10) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.25.10 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.1) + resolve-from: 5.0.0 + rollup: 4.52.4 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.6 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/tsconfig.json b/tsconfig.json index 8dae9b0..5783124 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,6 +34,10 @@ // Playground { "path": "./packages/playground/tsconfig.app.json" + }, + // CLI + { + "path": "./packages/cli/tsconfig.app.json" } ], "files": [] From ed0bcaa234ff987f4954947880e89429b630f457 Mon Sep 17 00:00:00 2001 From: Gleb Stepanov Date: Thu, 9 Oct 2025 23:36:00 +0200 Subject: [PATCH 03/10] fixed dts error to build CLI project --- packages/cli/package.json | 1 + packages/cli/{tsconfig.app.json => tsconfig.json} | 6 +++--- packages/cli/tsup.config.ts | 8 +++++++- tsconfig.json | 5 +---- 4 files changed, 12 insertions(+), 8 deletions(-) rename packages/cli/{tsconfig.app.json => tsconfig.json} (76%) diff --git a/packages/cli/package.json b/packages/cli/package.json index 5283e22..1adaebd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -11,6 +11,7 @@ "files": [ "bin", "dist", + "src", "templates" ], "scripts": { diff --git a/packages/cli/tsconfig.app.json b/packages/cli/tsconfig.json similarity index 76% rename from packages/cli/tsconfig.app.json rename to packages/cli/tsconfig.json index 0bcb658..8650f2c 100644 --- a/packages/cli/tsconfig.app.json +++ b/packages/cli/tsconfig.json @@ -1,16 +1,16 @@ { - "extends": "../../tsconfig.json", "compilerOptions": { - "composite": true, "target": "ES2022", "rootDir": "src", "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": false, + "strict": true, "declaration": true, "declarationMap": true, "emitDeclarationOnly": false, - "outDir": "dist" + "outDir": "dist", + "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.spec.ts"] diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts index f7860df..3e6b3bf 100644 --- a/packages/cli/tsup.config.ts +++ b/packages/cli/tsup.config.ts @@ -1,8 +1,14 @@ import { defineConfig } from 'tsup' export default defineConfig({ - entry: ['src/index.ts'], + entry: ['./src/index.ts'], format: ['esm'], outDir: 'dist', clean: true, + dts: { + resolve: true, + compilerOptions: { + composite: false, + }, + }, }) diff --git a/tsconfig.json b/tsconfig.json index 5783124..a25f723 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,11 +34,8 @@ // Playground { "path": "./packages/playground/tsconfig.app.json" - }, - // CLI - { - "path": "./packages/cli/tsconfig.app.json" } + // CLI ], "files": [] } From 572b9b0731fc3f97454ae387bf207b0fe4723abb Mon Sep 17 00:00:00 2001 From: Gleb Stepanov Date: Sat, 11 Oct 2025 00:07:32 +0200 Subject: [PATCH 04/10] Enhance project prompts with additional options for npm scope and package selection --- packages/cli/src/prompts/project.ts | 56 ++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/prompts/project.ts b/packages/cli/src/prompts/project.ts index 3a45eb0..924c280 100644 --- a/packages/cli/src/prompts/project.ts +++ b/packages/cli/src/prompts/project.ts @@ -24,8 +24,62 @@ export async function promptProjectOptions(targetDir: string): Promise value.trim() ? true : 'Project name cannot be empty', }, + { + type: 'text', + name: 'scope', + message: 'npm scope (optional, without @): ', + initial: '', + format: (value: string) => value.trim(), + }, + { + type: 'text', + name: 'packageName', + message: 'Package name (npm package name): ', + initial: 'lib', + validate: (value: string) => value.trim() ? true : 'Package name cannot be empty', + }, + { + type: 'multiselect', + name: 'packages', + message: 'Select packages to include: ', + choices: [ + { + title: 'Vue 3 library', + value: 'lib', + selected: false, + }, + { + title: 'TypeScript library', + value: 'libTs', + selected: false, + }, + { + title: `Playground (Vue 3 + Vite) ${chalk.bold('(recommended)')}`, + value: 'playground', + selected: true, + }, + ], + min: 1, + hint: '- Space to select. Return to submit', + instructions: false, + }, ], ) - return response as ProjectOptions | null + if (!response.projectName) { + return null + } + + const packages: string[] = response.packages || [] + + return { + projectName: response.projectName, + packageName: response.packageName, + scope: response.scope, + includeLib: packages.includes('lib'), + includeLibTs: packages.includes('libTs'), + includePlayground: packages.includes('playground'), + includeDocs: true, + installDeps: true, + } } From 2e24686c07ee8b81753f9c93e7546233e1429bea Mon Sep 17 00:00:00 2001 From: Gleb Stepanov Date: Sat, 11 Oct 2025 21:58:01 +0200 Subject: [PATCH 05/10] first structure of cli tool, but packages dont copy, weirdly enough --- packages/cli/package.json | 4 +- packages/cli/src/commands/create.ts | 59 +++++++ packages/cli/src/index.ts | 16 +- packages/cli/src/prompts/project.ts | 34 +++- packages/cli/src/templates/index.ts | 244 ++++++++++++++++++++++++++++ packages/cli/src/utils/fs.ts | 29 ++++ packages/cli/src/utils/install.ts | 21 +++ packages/cli/src/utils/replace.ts | 70 ++++++++ pnpm-lock.yaml | 67 +++++++- 9 files changed, 529 insertions(+), 15 deletions(-) create mode 100644 packages/cli/src/commands/create.ts create mode 100644 packages/cli/src/templates/index.ts create mode 100644 packages/cli/src/utils/fs.ts create mode 100644 packages/cli/src/utils/install.ts create mode 100644 packages/cli/src/utils/replace.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 1adaebd..1c1b406 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -20,7 +20,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "chalk": "^5.6.2", + "fast-glob": "^3.3.3", + "ora": "^9.0.0", + "picocolors": "^1.1.1", "prompts": "^2.4.2" }, "devDependencies": { diff --git a/packages/cli/src/commands/create.ts b/packages/cli/src/commands/create.ts new file mode 100644 index 0000000..a4e7ed1 --- /dev/null +++ b/packages/cli/src/commands/create.ts @@ -0,0 +1,59 @@ +import { resolve } from 'node:path' +import process from 'node:process' +import ora from 'ora' +import pc from 'picocolors' +import { promptProjectOptions } from '../prompts/project' +import { scaffoldProject } from '../templates/index' +import { ensureDir, isDirEmpty } from '../utils/fs' +import { installDependencies } from '../utils/install' + +export async function createCommand(targetDir: string): Promise { + const root = resolve(process.cwd(), targetDir) + + // Check if directory is empty + const isEmpty = await isDirEmpty(root) + if (!isEmpty) { + console.error( + pc.red(`āœ– Directory ${pc.bold(targetDir)} is not empty`), + ) + process.exit(1) + } + + // Ensure directory exists + await ensureDir(root) + + // Get user configuration + const config = await promptProjectOptions(targetDir) + if (!config) { + process.exit(0) + } + + // Scaffold project + const spinner = ora('Creating project structure...').start() + + try { + await scaffoldProject(root, config) + spinner.succeed('Project structure created') + } + catch (err) { + spinner.fail('Failed to create project structure') + console.error(err) + process.exit(1) + } + + // Install dependencies + if (config.installDeps) { + await installDependencies(root) + } + + // Success message + console.log(`\n${pc.green('āœ”')} Project created successfully!\n`) + console.log(pc.bold('Next steps:')) + console.log(` ${pc.cyan('cd')} ${config.projectName}`) + if (!config.installDeps) { + console.log(` ${pc.cyan('pnpm install')}`) + } + console.log(` ${pc.cyan('pnpm dev')}\n`) + + console.log(pc.dim('Happy coding! šŸš€\n')) +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index e6b1480..d6b5264 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,7 +1,15 @@ #!/usr/bin/env node +import process from 'node:process' +import { createCommand } from './commands/create' -import { promptProjectOptions } from './prompts/project' +async function main() { + const args = process.argv.slice(2) + const targetDir = args[0] || '.' -console.log('Hello, CLI!') -const val = await promptProjectOptions('lib') -console.log(val) + await createCommand(targetDir) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/packages/cli/src/prompts/project.ts b/packages/cli/src/prompts/project.ts index 924c280..2e5af4f 100644 --- a/packages/cli/src/prompts/project.ts +++ b/packages/cli/src/prompts/project.ts @@ -1,4 +1,5 @@ -import chalk from 'chalk' +import process from 'node:process' +import pc from 'picocolors' import prompts from 'prompts' export interface ProjectOptions { @@ -13,7 +14,7 @@ export interface ProjectOptions { } export async function promptProjectOptions(targetDir: string): Promise { - console.log(`\n${chalk.bold.cyan('Create a Vue/TS Library project')}\n`) + console.log(`\n${pc.bold(pc.cyan('Create a Vue/TS Library project'))}\n`) const response = await prompts( [ @@ -54,32 +55,51 @@ export async function promptProjectOptions(targetDir: string): Promise { + console.log(`${pc.red('\nāœ–')} Operation cancelled.`) + process.exit(0) + }, + }, ) if (!response.projectName) { return null } - const packages: string[] = response.packages || [] + const packages: string[] = response.packages ?? [] - return { + const options: ProjectOptions = { projectName: response.projectName, packageName: response.packageName, scope: response.scope, includeLib: packages.includes('lib'), includeLibTs: packages.includes('libTs'), includePlayground: packages.includes('playground'), - includeDocs: true, - installDeps: true, + includeDocs: packages.includes('docs'), + installDeps: response.installDeps, } + + return options } diff --git a/packages/cli/src/templates/index.ts b/packages/cli/src/templates/index.ts new file mode 100644 index 0000000..e09bd34 --- /dev/null +++ b/packages/cli/src/templates/index.ts @@ -0,0 +1,244 @@ +import type { ProjectOptions } from '../prompts/project' +import { writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { copyDir, readJson, writeJson } from '../utils/fs' +import { replaceInFiles } from '../utils/replace' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +const packagesDir = join(__dirname, '../../..') + +export async function scaffoldProject(targetDir: string, config: ProjectOptions): Promise { + await createBaseFiles(targetDir, config) +} + +// TODO: adjust content of package.json, programmatically +async function createBaseFiles(targetDir: string, config: ProjectOptions): Promise { + const rootPkg = { + name: config.projectName, + type: 'module', + version: '0.1.0', + private: true, + packageManager: 'pnpm@10.18.1', + description: `${config.projectName} - A Vue/TS library`, + author: 'Your name ', + license: 'MIT', + engines: { + node: '>=24.7.0', + pnpm: '>=10.16.1', + }, + scripts: { + 'dev': 'pnpm -F playground dev', + 'test': 'pnpm --if-present -r run test', + 'test-ci': 'pnpm --if-present -r run test-ci', + 'docs': 'pnpm -F docs run dev', + 'docs-build': 'pnpm -F docs run build', + 'lint': 'eslint .', + 'lint-fix': 'eslint . --fix', + 'build': buildScripts(config), + }, + devDependencies: { + '@antfu/eslint-config': '^5.4.1', + '@tsconfig/node24': '^24.0.1', + '@types/node': '24.6.2', + '@vitejs/plugin-vue': '^6.0.1', + '@vue/compiler-dom': '^3.5.22', + '@vue/test-utils': '^2.4.6', + '@vue/tsconfig': '^0.8.1', + 'eslint': '^9.36.0', + 'eslint-plugin-format': '^1.0.1', + 'jsdom': '^27.0.0', + 'typescript': '^5.9.0', + 'vite': '^7.1.0', + 'vitest': '^3.2.4', + 'vue': '^3.5.0', + 'vue-tsc': '^3.0.0', + }, + } + + await writeJson(join(targetDir, 'package.json'), rootPkg) + + const configFiles = [ + 'tsconfig.json', + 'tsconfig.config.json', + 'eslint.config.js', + ] + + for (const file of configFiles) { + const sourcePath = join(packagesDir, file) + const destPath = join(targetDir, file) + await copyDir(sourcePath, destPath) + } + + const gitignore = `# Dependencies +node_modules/ +.pnpm-store/ + +# Build outputs +dist/ +*.tsbuildinfo + +# Environment +.env +.env.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +pnpm-debug.log* +` + + await writeFile(join(targetDir, '.gitignore'), gitignore) + + const readme = `# ${config.projectName} +A Vue/TypeScript library created with @glstep/app + +## Development + +\`\`\`bash +# Install dependencies +pnpm install + +# Start playground +pnpm dev + +# Run tests +pnpm test + +# Build +pnpm build +\`\`\` + +## Packages + +${config.includeLib ? '- **lib**: Vue component library\n' : ''}${config.includeLibTs ? '- **lib-ts**: TypeScript utility library\n' : ''}${config.includePlayground ? '- **playground**: Development playground\n' : ''}${config.includeDocs ? '- **docs**: Documentation site\n' : ''} +` + + await writeFile(join(targetDir, 'README.md'), readme) +} + +function buildScripts(config: ProjectOptions): string { + const scripts: string[] = [] + + if (config.includeLib) + scripts.push('pnpm -F @SCOPE@/lib run build') + if (config.includeLibTs) + scripts.push('pnpm -F @SCOPE@/lib-ts run build') + if (config.includePlayground) + scripts.push('pnpm -F playground run build') + if (config.includeDocs) + scripts.push('pnpm -F docs run build') + + return scripts.join(' && ') +} + +async function copyPackage(packageName: string, targetDir: string): Promise { + const sourceDir = join(packagesDir, 'packages', packageName) + const destDir = join(targetDir, 'packages', packageName) + + await copyDir(sourceDir, destDir) +} + +async function createWorkspaceFile(targetDir: string): Promise { + const content = `packages: + - 'packages/*' + ` + + await writeFile(join(targetDir, 'pnpm-workspace.yaml'), content) +} + +async function replacePlaceholders(targetDir: string, config: ProjectOptions): Promise { + const fullScope = config.scope ? `@${config.scope}` : '' + const fullLibName = fullScope ? `${fullScope}/${config.packageName}` : config.packageName + + const replacements = [ + { from: /@glstep\/lib-ts/g, to: fullScope ? `${fullScope}/lib-ts` : 'lib-ts' }, + { from: /@glstep\/lib/g, to: fullLibName }, + { from: /@glstep/g, to: fullScope }, + { from: /vue-lib-monorepo-template/g, to: config.projectName }, + { from: /Gleb Stepanov /g, to: 'Your Name ' }, + { from: '@SCOPE@', to: fullScope || config.projectName }, + ] + + await replaceInFiles(targetDir, replacements) + + await updatePackageJsonFiles(targetDir, config, fullScope, fullLibName) +} + +async function updatePackageJsonFiles( + targetDir: string, + config: ProjectOptions, + fullScope: string, + fullLibName: string, +): Promise { + const updates = [] + + if (config.includeLib) { + updates.push({ + path: join(targetDir, 'packages', 'lib', 'package.json'), + name: fullLibName, + }) + } + + if (config.includeLibTs) { + updates.push({ + path: join(targetDir, 'packages', 'lib-ts', 'package.json'), + name: fullScope ? `${fullScope}/lib-ts` : 'lib-ts', + }) + } + + if (config.includePlayground) { + updates.push({ + path: join(targetDir, 'packages', 'playground', 'package.json'), + name: 'playground', + }) + } + + if (config.includeDocs) { + updates.push({ + path: join(targetDir, 'packages', 'docs', 'package.json'), + name: 'docs', + }) + } + + for (const { path, name } of updates) { + try { + const pkg = await readJson(path) + pkg.name = name + + // Update dependencies + if (pkg.dependencies) { + const newDeps: Record = {} + for (const [key, value] of Object.entries(pkg.dependencies)) { + if (key === '@glstep/lib') { + newDeps[fullLibName] = value as string + } + else if (key.startsWith('@glstep/')) { + const pkgName = key.split('/')[1] + newDeps[fullScope ? `${fullScope}/${pkgName}` : pkgName] = value as string + } + else { + newDeps[key] = value as string + } + } + pkg.dependencies = newDeps + } + + await writeJson(path, pkg) + } + catch (err) { + console.warn(`Warning: Could not update package.json at ${path}: ${(err as Error).message}`) + } + } +} diff --git a/packages/cli/src/utils/fs.ts b/packages/cli/src/utils/fs.ts new file mode 100644 index 0000000..2e203ef --- /dev/null +++ b/packages/cli/src/utils/fs.ts @@ -0,0 +1,29 @@ +import { existsSync } from 'node:fs' +import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises' + +export async function ensureDir(dir: string): Promise { + if (!existsSync(dir)) { + await mkdir(dir, { recursive: true }) + } +} + +export async function isDirEmpty(dir: string): Promise { + if (!existsSync(dir)) { + return true + } + const files = await readdir(dir) + return files.length === 0 +} + +export async function copyDir(src: string, dest: string): Promise { + await cp(src, dest, { recursive: true, force: true }) +} + +export async function readJson(path: string): Promise { + const content = await readFile(path, 'utf-8') + return JSON.parse(content) +} + +export async function writeJson(path: string, data: any): Promise { + await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, 'utf-8') +} diff --git a/packages/cli/src/utils/install.ts b/packages/cli/src/utils/install.ts new file mode 100644 index 0000000..ecd6eee --- /dev/null +++ b/packages/cli/src/utils/install.ts @@ -0,0 +1,21 @@ +import { execSync } from 'node:child_process' +import process from 'node:process' +import ora from 'ora' +import pc from 'picocolors' + +export async function installDependencies(cwd: string): Promise { + const spinner = ora('Installing dependencies...').start() + + try { + execSync('pnpm install', { + cwd, + stdio: 'ignore', + }) + spinner.succeed('Dependencies installed successfully.') + } + catch { + spinner.fail('Failed to install dependencies.') + console.log(`\n${pc.yellow('Please run "pnpm install" manually.\n')}`) + process.exit(0) + } +} diff --git a/packages/cli/src/utils/replace.ts b/packages/cli/src/utils/replace.ts new file mode 100644 index 0000000..14177cf --- /dev/null +++ b/packages/cli/src/utils/replace.ts @@ -0,0 +1,70 @@ +import { readFile, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import fg from 'fast-glob' + +export interface Replacement { + from: string | RegExp + to: string +} + +export async function replaceInFiles(dir: string, replacements: Replacement[]): Promise { + const files = await fg('**/*', { + cwd: dir, + ignore: ['node_modules/**', '**/dist/**', '.git/**'], + dot: true, + onlyFiles: true, + }) + + const textExtensions = [ + '.js', + '.mjs', + '.cjs', + '.ts', + '.tsx', + '.jsx', + '.vue', + '.json', + '.html', + '.css', + '.scss', + '.md', + '.yml', + '.yaml', + '.txt', + ] + + for (const file of files) { + const filePath = join(dir, file) + const ext = file.substring(file.lastIndexOf('.')) + + if (!textExtensions.includes(ext) && !file.endsWith('package.json')) { + continue + } + + try { + let content = await readFile(filePath, 'utf-8') + let modified = false + + for (const { from, to } of replacements) { + const oldContent = content + if (typeof from === 'string') { + content = content.split(from).join(to) + } + else { + content = content.replace(from, to) + } + + if (content !== oldContent) { + modified = true + } + } + + if (modified) { + await writeFile(filePath, content, 'utf-8') + } + } + catch (err) { + console.error(`Error processing file ${filePath}:`, err) + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ddc6129..304c533 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,9 +62,15 @@ importers: packages/cli: dependencies: - chalk: - specifier: ^5.6.2 - version: 5.6.2 + fast-glob: + specifier: ^3.3.3 + version: 3.3.3 + ora: + specifier: ^9.0.0 + version: 9.0.0 + picocolors: + specifier: ^1.1.1 + version: 1.1.1 prompts: specifier: ^2.4.2 version: 2.4.2 @@ -1567,6 +1573,10 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} + cli-spinners@3.3.0: + resolution: {integrity: sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==} + engines: {node: '>=18.20'} + cli-truncate@5.1.0: resolution: {integrity: sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==} engines: {node: '>=20'} @@ -2204,6 +2214,10 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2215,6 +2229,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2348,6 +2366,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -2614,6 +2636,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@9.0.0: + resolution: {integrity: sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==} + engines: {node: '>=20'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2972,6 +2998,10 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -3511,6 +3541,10 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -4785,6 +4819,8 @@ snapshots: dependencies: restore-cursor: 5.1.0 + cli-spinners@3.3.0: {} + cli-truncate@5.1.0: dependencies: slice-ansi: 7.1.2 @@ -5518,12 +5554,16 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-interactive@2.0.0: {} + is-number@7.0.0: {} is-potential-custom-element-name@1.0.1: {} is-stream@2.0.1: {} + is-unicode-supported@2.1.0: {} + isexe@2.0.0: {} jackspeak@3.4.3: @@ -5661,6 +5701,11 @@ snapshots: lodash@4.17.21: {} + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.1.2 + log-update@6.1.0: dependencies: ansi-escapes: 7.1.1 @@ -6099,6 +6144,18 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@9.0.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.3.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.2.2 + string-width: 8.1.0 + strip-ansi: 7.1.2 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -6411,6 +6468,8 @@ snapshots: std-env@3.9.0: {} + stdin-discarder@0.2.2: {} + string-argv@0.3.2: {} string-width@4.2.3: @@ -6984,4 +7043,6 @@ snapshots: yocto-queue@1.2.1: {} + yoctocolors@2.1.2: {} + zwitch@2.0.4: {} From c1b0c58ebbe3dec80b1375727dbaf17a5bc3c316 Mon Sep 17 00:00:00 2001 From: Gleb Stepanov Date: Sun, 12 Oct 2025 19:19:11 +0200 Subject: [PATCH 06/10] scaffold testing environment for unit tests, e2e, integration, etc. --- packages/cli/package.json | 8 +- packages/cli/src/__tests__/index.spec.ts | 15 --- .../cli/src/commands/__tests__/create.spec.ts | 0 .../cli/src/prompts/__tests__/project.spec.ts | 0 .../cli/src/templates/__tests__/index.spec.ts | 0 packages/cli/src/utils/__tests__/fs.spec.ts | 0 .../cli/src/utils/__tests__/install.spec.ts | 0 .../cli/src/utils/__tests__/replace.spec.ts | 0 packages/cli/tsconfig.json | 2 +- packages/cli/vitest.config.ts | 16 +++ pnpm-lock.yaml | 112 ++++++++++++++++++ 11 files changed, 136 insertions(+), 17 deletions(-) delete mode 100644 packages/cli/src/__tests__/index.spec.ts create mode 100644 packages/cli/src/commands/__tests__/create.spec.ts create mode 100644 packages/cli/src/prompts/__tests__/project.spec.ts create mode 100644 packages/cli/src/templates/__tests__/index.spec.ts create mode 100644 packages/cli/src/utils/__tests__/fs.spec.ts create mode 100644 packages/cli/src/utils/__tests__/install.spec.ts create mode 100644 packages/cli/src/utils/__tests__/replace.spec.ts create mode 100644 packages/cli/vitest.config.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 1c1b406..b747ed2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -17,15 +17,21 @@ "scripts": { "build": "tsup src/index.ts --format esm --dts", "dev": "tsup src/index.ts --format esm --watch", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest", + "test-run": "vitest run", + "test-unit": "vitest run src", + "test-e2e": "vitest run tests/e2e" }, "dependencies": { + "execa": "^9.6.0", "fast-glob": "^3.3.3", "ora": "^9.0.0", "picocolors": "^1.1.1", "prompts": "^2.4.2" }, "devDependencies": { + "@types/node": "^24.6.2", "@types/prompts": "^2.4.9", "tsup": "^8.5.0", "typescript": "^5.9.0", diff --git a/packages/cli/src/__tests__/index.spec.ts b/packages/cli/src/__tests__/index.spec.ts deleted file mode 100644 index 129ef7d..0000000 --- a/packages/cli/src/__tests__/index.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { execSync } from 'node:child_process' -import { resolve } from 'node:path' -import { describe, expect, it } from 'vitest' - -describe('cli', () => { - it('should output message', () => { - const binPath = resolve(__dirname, '../../bin/create-app.mjs') - - const output = execSync(`node ${binPath}`, { - encoding: 'utf-8', - }) - - expect(output).toContain('Hello, CLI!') - }) -}) diff --git a/packages/cli/src/commands/__tests__/create.spec.ts b/packages/cli/src/commands/__tests__/create.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/cli/src/prompts/__tests__/project.spec.ts b/packages/cli/src/prompts/__tests__/project.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/cli/src/templates/__tests__/index.spec.ts b/packages/cli/src/templates/__tests__/index.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/cli/src/utils/__tests__/fs.spec.ts b/packages/cli/src/utils/__tests__/fs.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/cli/src/utils/__tests__/install.spec.ts b/packages/cli/src/utils/__tests__/install.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/cli/src/utils/__tests__/replace.spec.ts b/packages/cli/src/utils/__tests__/replace.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 8650f2c..965d5e0 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -12,6 +12,6 @@ "outDir": "dist", "forceConsistentCasingInFileNames": true }, - "include": ["src/**/*"], + "include": ["src/**/*", "vitest.config.ts"], "exclude": ["node_modules", "dist", "**/*.spec.ts"] } diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts new file mode 100644 index 0000000..3235eef --- /dev/null +++ b/packages/cli/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.spec.ts', 'tests/**/*.spec.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'html', 'lcov'], + include: ['src/**/*.ts'], + exclude: ['src/**/*.spec.ts', 'tests/**/*.ts', 'node_modules/**', 'dist/**'], + }, + testTimeout: 15000, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 304c533..dd27dff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: packages/cli: dependencies: + execa: + specifier: ^9.6.0 + version: 9.6.0 fast-glob: specifier: ^3.3.3 version: 3.3.3 @@ -75,6 +78,9 @@ importers: specifier: ^2.4.2 version: 2.4.2 devDependencies: + '@types/node': + specifier: ^24.6.2 + version: 24.6.2 '@types/prompts': specifier: ^2.4.9 version: 2.4.9 @@ -1056,9 +1062,16 @@ packages: cpu: [x64] os: [win32] + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@stylistic/eslint-plugin@5.4.0': resolution: {integrity: sha512-UG8hdElzuBDzIbjG1QDwnYH0MQ73YLXDFHgZzB4Zh/YJfnw8XNsloVtytqzx0I2Qky9THSdpTmi8Vjn/pf/Lew==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1995,6 +2008,10 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} @@ -2033,6 +2050,10 @@ packages: picomatch: optional: true + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2092,6 +2113,10 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} @@ -2159,6 +2184,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -2222,6 +2251,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -2229,6 +2262,10 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-unicode-supported@2.1.0: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} @@ -2610,6 +2647,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -2669,6 +2710,10 @@ packages: parse-imports-exports@0.2.4: resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parse-statements@1.0.11: resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} @@ -2686,6 +2731,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -2826,6 +2875,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -3034,6 +3087,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-indent@4.1.0: resolution: {integrity: sha512-OA95x+JPmL7kc7zCu+e+TeYxEiaIyndRx0OrBcK2QPPH09oAndr2ALvymxWA+Lx1PYYvFUm4O63pRkdJAaW96w==} engines: {node: '>=12'} @@ -3229,6 +3286,10 @@ packages: undici-types@7.13.0: resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} @@ -4247,8 +4308,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.4': optional: true + '@sec-ant/readable-stream@0.4.1': {} + '@sinclair/typebox@0.27.8': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@stylistic/eslint-plugin@5.4.0(eslint@9.37.0(jiti@1.21.7))': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@1.21.7)) @@ -5357,6 +5422,21 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + execa@9.6.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + expect-type@1.2.2: {} exsolve@1.0.7: {} @@ -5389,6 +5469,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -5441,6 +5525,11 @@ snapshots: get-stream@6.0.1: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -5511,6 +5600,8 @@ snapshots: human-signals@2.1.0: {} + human-signals@8.0.1: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -5558,10 +5649,14 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} is-stream@2.0.1: {} + is-stream@4.0.1: {} + is-unicode-supported@2.1.0: {} isexe@2.0.0: {} @@ -6115,6 +6210,11 @@ snapshots: dependencies: path-key: 3.1.1 + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -6182,6 +6282,8 @@ snapshots: dependencies: parse-statements: 1.0.11 + parse-ms@4.0.0: {} + parse-statements@1.0.11: {} parse5@7.3.0: @@ -6194,6 +6296,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-scurry@1.11.1: @@ -6302,6 +6406,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -6505,6 +6613,8 @@ snapshots: strip-final-newline@2.0.0: {} + strip-final-newline@4.0.0: {} + strip-indent@4.1.0: {} strip-json-comments@3.1.1: {} @@ -6711,6 +6821,8 @@ snapshots: undici-types@7.13.0: {} + unicorn-magic@0.3.0: {} + unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.3 From 7946bed2cefdf4498acf698517ef5c761c3ebc25 Mon Sep 17 00:00:00 2001 From: Gleb Stepanov Date: Sun, 12 Oct 2025 22:28:27 +0200 Subject: [PATCH 07/10] first test --- packages/cli/src/utils/__tests__/fs.spec.ts | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/cli/src/utils/__tests__/fs.spec.ts b/packages/cli/src/utils/__tests__/fs.spec.ts index e69de29..d9f6de6 100644 --- a/packages/cli/src/utils/__tests__/fs.spec.ts +++ b/packages/cli/src/utils/__tests__/fs.spec.ts @@ -0,0 +1,29 @@ +import { existsSync, mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { ensureDir } from '../fs' + +describe('fs utilities', () => { + let tempDir: string + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'fs-utils-test')) + }) + + afterEach(() => { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }) + } + }) + + describe('ensureDir', () => { + it('create new dir if does not exist', async () => { + const newDir = join(tempDir, 'new-dir') + + expect(existsSync(newDir)).toBe(false) + await ensureDir(newDir) + expect(existsSync(newDir)).toBe(true) + }) + }) +}) From 1bf708e6b70867551d2058f522ebac21d6ab5c61 Mon Sep 17 00:00:00 2001 From: Gleb Stepanov Date: Mon, 13 Oct 2025 14:43:31 +0200 Subject: [PATCH 08/10] expand on tests --- packages/cli/src/utils/__tests__/fs.spec.ts | 47 +++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/utils/__tests__/fs.spec.ts b/packages/cli/src/utils/__tests__/fs.spec.ts index d9f6de6..db8a8b1 100644 --- a/packages/cli/src/utils/__tests__/fs.spec.ts +++ b/packages/cli/src/utils/__tests__/fs.spec.ts @@ -1,8 +1,8 @@ -import { existsSync, mkdtempSync, rmSync } from 'node:fs' +import { existsSync, mkdtempSync, rmSync, writeFile } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { ensureDir } from '../fs' +import { ensureDir, isDirEmpty } from '../fs' describe('fs utilities', () => { let tempDir: string @@ -18,12 +18,53 @@ describe('fs utilities', () => { }) describe('ensureDir', () => { - it('create new dir if does not exist', async () => { + it('should create new dir if does not exist', async () => { const newDir = join(tempDir, 'new-dir') + console.log('Temporary directory for test: ', newDir) expect(existsSync(newDir)).toBe(false) await ensureDir(newDir) expect(existsSync(newDir)).toBe(true) }) + + it('should not fail, if dir already exists', async () => { + const existingDir = join(tempDir, 'existing-dir') + await ensureDir(existingDir) + console.log('Temporary directory for test: ', existingDir) + await expect(ensureDir(existingDir)).resolves.toBeUndefined() + expect(existsSync(existingDir)).toBe(true) + }) + + it('should create nested directories', async () => { + const nestedDir = join(tempDir, 'level1', 'level2', 'level3') + console.log('Temporary directory for test: ', nestedDir) + await ensureDir(nestedDir) + expect(existsSync(nestedDir)).toBe(true) + }) + }) + + describe('isDirEmpty', () => { + it('should return true for non existent dir', async () => { + const nonExistentDir = join(tempDir, 'non-existent') + expect(existsSync(nonExistentDir)).toBe(false) + const result = await isDirEmpty(nonExistentDir) + expect(result).toBe(true) + }) + + it('should return true for empty dir', async () => { + const emptyDir = join(tempDir, 'empty-dir') + console.log('Temporary directory for test: ', emptyDir) + await ensureDir(emptyDir) + const result = await isDirEmpty(emptyDir) + expect(result).toBe(true) + }) + + it('should return false for non empty dir', async () => { + const nonEmptyDir = join(tempDir, 'non-empty-dir') + console.log('Temporary directory for test: ', nonEmptyDir) + writeFile(join(nonEmptyDir, 'file.txt'), 'content') + const result = await isDirEmpty(nonEmptyDir) + expect(result).toBe(false) + }) }) }) From f8dc7812dc5da096a43fa1bddccb15baa992e25e Mon Sep 17 00:00:00 2001 From: Gleb Stepanov Date: Mon, 13 Oct 2025 23:33:26 +0200 Subject: [PATCH 09/10] expand fs tests --- packages/cli/src/utils/__tests__/fs.spec.ts | 58 +++++++++++++++++---- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/utils/__tests__/fs.spec.ts b/packages/cli/src/utils/__tests__/fs.spec.ts index db8a8b1..40b74cf 100644 --- a/packages/cli/src/utils/__tests__/fs.spec.ts +++ b/packages/cli/src/utils/__tests__/fs.spec.ts @@ -1,8 +1,8 @@ -import { existsSync, mkdtempSync, rmSync, writeFile } from 'node:fs' +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { ensureDir, isDirEmpty } from '../fs' +import { copyDir, ensureDir, isDirEmpty } from '../fs' describe('fs utilities', () => { let tempDir: string @@ -20,8 +20,6 @@ describe('fs utilities', () => { describe('ensureDir', () => { it('should create new dir if does not exist', async () => { const newDir = join(tempDir, 'new-dir') - console.log('Temporary directory for test: ', newDir) - expect(existsSync(newDir)).toBe(false) await ensureDir(newDir) expect(existsSync(newDir)).toBe(true) @@ -30,14 +28,12 @@ describe('fs utilities', () => { it('should not fail, if dir already exists', async () => { const existingDir = join(tempDir, 'existing-dir') await ensureDir(existingDir) - console.log('Temporary directory for test: ', existingDir) await expect(ensureDir(existingDir)).resolves.toBeUndefined() expect(existsSync(existingDir)).toBe(true) }) it('should create nested directories', async () => { const nestedDir = join(tempDir, 'level1', 'level2', 'level3') - console.log('Temporary directory for test: ', nestedDir) await ensureDir(nestedDir) expect(existsSync(nestedDir)).toBe(true) }) @@ -53,7 +49,6 @@ describe('fs utilities', () => { it('should return true for empty dir', async () => { const emptyDir = join(tempDir, 'empty-dir') - console.log('Temporary directory for test: ', emptyDir) await ensureDir(emptyDir) const result = await isDirEmpty(emptyDir) expect(result).toBe(true) @@ -61,10 +56,55 @@ describe('fs utilities', () => { it('should return false for non empty dir', async () => { const nonEmptyDir = join(tempDir, 'non-empty-dir') - console.log('Temporary directory for test: ', nonEmptyDir) - writeFile(join(nonEmptyDir, 'file.txt'), 'content') + await ensureDir(nonEmptyDir) + writeFileSync(join(nonEmptyDir, 'file.txt'), 'Hello, World!', { encoding: 'utf8', flag: 'wx' }) const result = await isDirEmpty(nonEmptyDir) expect(result).toBe(false) }) + + it('should return false for dir with sub-dir', async () => { + const dirWithSubDir = join(tempDir, 'dir-with-sub-dir') + const subDir = join(dirWithSubDir, 'sub-dir') + await ensureDir(subDir) + const result = await isDirEmpty(dirWithSubDir) + expect(result).toBe(false) + }) + + it('should return false for dir with hidden files', async () => { + const dirWithHiddenFile = join(tempDir, 'dir-with-hidden-file') + await ensureDir(dirWithHiddenFile) + writeFileSync(join(dirWithHiddenFile, '.hiddenfile'), 'This is a hidden file', { encoding: 'utf8', flag: 'wx' }) + const result = await isDirEmpty(dirWithHiddenFile) + expect(result).toBe(false) + }) + + it('should return false for dir with multiple items', async () => { + const dirWithMultipleItems = join(tempDir, 'dir-with-multiple-items') + await ensureDir(dirWithMultipleItems) + writeFileSync(join(dirWithMultipleItems, 'file1.txt'), 'File 1', { encoding: 'utf8', flag: 'wx' }) + writeFileSync(join(dirWithMultipleItems, 'file2.txt'), 'File 2', { encoding: 'utf8', flag: 'wx' }) + await ensureDir(join(dirWithMultipleItems, 'sub-dir')) + const result = await isDirEmpty(dirWithMultipleItems) + expect(result).toBe(false) + }) + }) + + describe('copyDir', () => { + it('should copy directory with all files', async () => { + const srcDir = join(tempDir, 'src') + const destDir = join(tempDir, 'dest') + + await ensureDir(srcDir) + writeFileSync(join(srcDir, 'file1.txt'), 'File 1', { encoding: 'utf8', flag: 'wx' }) + writeFileSync(join(srcDir, 'file2.txt'), 'File 2', { encoding: 'utf8', flag: 'wx' }) + + await copyDir(srcDir, destDir) + + expect(existsSync(destDir)).toBe(true) + expect(existsSync(join(destDir, 'file1.txt'))).toBe(true) + expect(existsSync(join(destDir, 'file2.txt'))).toBe(true) + expect(readFileSync(join(destDir, 'file1.txt'), 'utf8')).toBe('File 1') + expect(readFileSync(join(destDir, 'file2.txt'), 'utf8')).toBe('File 2') + }) }) }) From 7214bb6e1287acae6f3c4258ca9193f48eb78bcc Mon Sep 17 00:00:00 2001 From: Gleb Stepanov Date: Tue, 14 Oct 2025 13:22:22 +0200 Subject: [PATCH 10/10] first version of filesystem tests completed, may expand with further tests --- packages/cli/src/utils/__tests__/fs.spec.ts | 87 ++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/utils/__tests__/fs.spec.ts b/packages/cli/src/utils/__tests__/fs.spec.ts index 40b74cf..e81bc83 100644 --- a/packages/cli/src/utils/__tests__/fs.spec.ts +++ b/packages/cli/src/utils/__tests__/fs.spec.ts @@ -2,7 +2,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'no import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { copyDir, ensureDir, isDirEmpty } from '../fs' +import { copyDir, ensureDir, isDirEmpty, readJson, writeJson } from '../fs' describe('fs utilities', () => { let tempDir: string @@ -106,5 +106,90 @@ describe('fs utilities', () => { expect(readFileSync(join(destDir, 'file1.txt'), 'utf8')).toBe('File 1') expect(readFileSync(join(destDir, 'file2.txt'), 'utf8')).toBe('File 2') }) + + it('should copy dir with nested structure', async () => { + const srcDir = join(tempDir, 'src-nested') + await ensureDir(join(srcDir, 'level1', 'level2')) + writeFileSync(join(srcDir, 'level1', 'level2', 'file.txt'), 'Nested File', { encoding: 'utf8', flag: 'wx' }) + writeFileSync(join(srcDir, 'level1', 'file.txt'), 'Another nested File', { encoding: 'utf8', flag: 'wx' }) + writeFileSync(join(srcDir, 'file.txt'), 'Root File', { encoding: 'utf8', flag: 'wx' }) + + const destDir = join(tempDir, 'dest-nested') + await copyDir(srcDir, destDir) + + expect(existsSync(join(destDir, 'file.txt'))).toBe(true) + expect(readFileSync(join(destDir, 'file.txt'), 'utf8')).toBe('Root File') + + expect(existsSync(join(destDir, 'level1', 'file.txt'))).toBe(true) + expect(readFileSync(join(destDir, 'level1', 'file.txt'), 'utf8')).toBe('Another nested File') + + expect(existsSync(join(destDir, 'level1', 'level2', 'file.txt'))).toBe(true) + expect(readFileSync(join(destDir, 'level1', 'level2', 'file.txt'), 'utf8')).toBe('Nested File') + }) + }) + + describe('readJson', async () => { + it('should read and parse JSON file', async () => { + const jsonFile = join(tempDir, 'data.json') + const jsonData = { + 'name': 'Test', + 'value': 42, + 'nested': { a: 1, b: [1, 2, 3] }, + 'array': [10, 20, 30], + 'arrayObject': [{ x: 1 }, { y: 2 }], + 'bool': true, + 'nullValue': null, + 'string-test': 'value', + } + writeFileSync(jsonFile, JSON.stringify(jsonData, null, 2), { encoding: 'utf8', flag: 'wx' }) + + const result = await readJson(jsonFile) + expect(result).toEqual(jsonData) + expect(result.name).toEqual(jsonData.name) + expect(result['string-test']).toEqual(jsonData['string-test']) + }) + + it('should throw error for invalid JSON', async () => { + const invalidJsonFile = join(tempDir, 'invalid.json') + writeFileSync(invalidJsonFile, '{ invalid json ', { encoding: 'utf8', flag: 'wx' }) + + const result = readJson(invalidJsonFile) + await expect(result).rejects.toThrow(SyntaxError) + }) + }) + + describe('writeJson', async () => { + it('should write JSON object to file', async () => { + const jsonFile = join(tempDir, 'output.json') + const jsonData = { + title: 'Output Test', + count: 100, + items: ['a', 'b', 'c'], + details: { key: 'value' }, + isActive: false, + nullField: null, + } + + await writeJson(jsonFile, jsonData) + + expect(existsSync(jsonFile)).toBe(true) + const fileContent = readFileSync(jsonFile, { encoding: 'utf8' }) + const parsedContent = JSON.parse(fileContent) + expect(parsedContent).toEqual(jsonData) + expect(fileContent.endsWith('\n')).toBe(true) + }) + + it('should overwrite existing file', async () => { + const jsonFile = join(tempDir, 'overwrite.json') + const initialData = { initial: true } + writeFileSync(jsonFile, JSON.stringify(initialData), { encoding: 'utf8', flag: 'wx' }) + + const newData = { updated: true, number: 123 } + await writeJson(jsonFile, newData) + + const fileContent = readFileSync(jsonFile, { encoding: 'utf8' }) + const parsedContent = JSON.parse(fileContent) + expect(parsedContent).toEqual(newData) + }) }) })