diff --git a/packages/create-robo/__tests__/config.test.ts b/packages/create-robo/__tests__/config.test.ts new file mode 100644 index 000000000..e85191963 --- /dev/null +++ b/packages/create-robo/__tests__/config.test.ts @@ -0,0 +1,350 @@ +import { promises as fs } from 'fs' +import path from 'node:path' +import { SpawnOptions, spawn } from 'node:child_process' +const FOLDER_PROJECTS_TEST_PATH = path.join(process.cwd(), '__tests__', 'projects') +const LOCAL_COPY_OF_ROBO_CREATE_PATH = path.join('..', '..', 'dist', 'index.js') +// Do not forget to install all required package managers ! + +// KNOWS ISSUES + +// None of the package manager specific tests are passing. + +/** + * Plain Javascript: JavaScript, zero features, and credentials. + Plain TypeScript: TypeScript, zero features, and credentials. + Standard JavaScript: JavaScript, recommended features, and credentials. + Standard TypeScript: TypeScript, recommended features, and credentials. + + Skipped Credentials JS: JavaScript, recommended features, and skip credentials. + Skipped Credentials TS: TypeScript, recommended features, and skip credentials. + + Standard JS Plugin: Same as Standard JS but as a plugin. + Standard TS Plugin: Same as Standard TS but as a plugin. + */ + +async function areCredentialsSet(project_path: string): Promise { + const envCreds = await fs.readFile(project_path) + const isCredentialsSet = envCreds.includes(`Credentials`) + return isCredentialsSet +} + +async function areFeaturesInstalled(features: string[], project_name: string): Promise { + const featuresInstalled: boolean[] = [] + + for (const feature of features) { + const feature_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, feature) + await fs + .access(feature_path) + .then(() => featuresInstalled.push(true)) + .catch(() => false) + } + + return featuresInstalled.filter((plugin) => plugin !== true).length <= 0 ? true : false +} + +async function arePluginsInstalled(plugins: string[], project_name: string): Promise { + const pluginsInstalled: boolean[] = [] + + for (const plugin of plugins) { + const plugin_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, 'config', 'plugins', `${plugin}.mjs`) + await fs + .access(plugin_path) + .then(() => pluginsInstalled.push(true)) + .catch(() => false) + } + + return pluginsInstalled.filter((plugin) => plugin !== true).length <= 0 ? true : false +} + +describe('Create Robos ', () => { + beforeAll(async () => { + await fs.rmdir(FOLDER_PROJECTS_TEST_PATH, { recursive: true }) + await fs.mkdir(FOLDER_PROJECTS_TEST_PATH) + }, 15000) + + // Common + + it('Plain Javascript: JavaScript, zero features, and credentials.', async () => { + const project_name = `JZFC` + await exec(`node ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -js -f '' `, true, { + cwd: FOLDER_PROJECTS_TEST_PATH + }) + + const env_file_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, `.env`) + const isCredentialsSet = await areCredentialsSet(env_file_path) + expect(isCredentialsSet).toBeTruthy() + }, 20000) + + it('Pain Typescript: TypeScript, zero features, and credentials.', async () => { + const project_name = `TZFC` + await exec(`node ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -ts -f '' `, true, { + cwd: FOLDER_PROJECTS_TEST_PATH + }) + + const env_file_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, `.env`) + const isCredentialsSet = await areCredentialsSet(env_file_path) + expect(isCredentialsSet).toBeTruthy() + }, 20000) + + it(`Standard JavaScript: JavaScript, recommended features, and credentials.`, async () => { + const project_name = `JRFC` + await exec(`node ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -js -f prettier,eslint`, true, { + cwd: FOLDER_PROJECTS_TEST_PATH + }) + + const env_file_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, `.env`) + const isCredentialsSet = await areCredentialsSet(env_file_path) + const featureInstalled = areFeaturesInstalled(['prettier.config.mjs', '.eslintrc.json'], project_name) + + if (featureInstalled) { + expect(isCredentialsSet).toBeTruthy() + } + }, 20000) + + it(`Standard TypeScript: TypeScript, recommended features, and credentials.`, async () => { + const project_name = `TRFC` + await exec(`node ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -ts -f prettier,eslint`, true, { + cwd: FOLDER_PROJECTS_TEST_PATH + }) + + const env_file_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, `.env`) + + const isCredentialsSet = await areCredentialsSet(env_file_path) + const featureInstalled = areFeaturesInstalled(['prettier.config.mjs', '.eslintrc.json'], project_name) + + if (featureInstalled) { + expect(isCredentialsSet).toBeTruthy() + } + }, 20000) + + it(`Skipped Credentials JS: JavaScript, recommended features, skip credentials.`, async () => { + const project_name = `JRFSC` + await exec(`node ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -js -f prettier,eslint`, false, { + cwd: FOLDER_PROJECTS_TEST_PATH + }) + + const env_file_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, `.env`) + + const isCredentialsSet = await areCredentialsSet(env_file_path) + const featureInstalled = areFeaturesInstalled(['prettier.config.mjs', '.eslintrc.json'], project_name) + + if (featureInstalled) { + expect(isCredentialsSet).toBeFalsy() + } + }, 20000) + + it(`Skipped Credentials TS: TypeScript, recommended features, skip credentials.`, async () => { + const project_name = `TRFSC` + await exec(`node ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -ts -f prettier,eslint`, false, { + cwd: FOLDER_PROJECTS_TEST_PATH + }) + + const env_file_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, `.env`) + + const isCredentialsSet = await areCredentialsSet(env_file_path) + const featureInstalled = areFeaturesInstalled(['prettier.config.mjs', '.eslintrc.json'], project_name) + + if (featureInstalled) { + expect(isCredentialsSet).toBeFalsy() + } + }, 20000) + + it(`Standard JS Plugin: Same as Standard JS but as a plugin.`, async () => { + const project_name = 'JSTANDARP' + await exec(`node ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -js --plugin -f prettier,eslint`, true, { + cwd: FOLDER_PROJECTS_TEST_PATH + }) + + const md_file_path = fs + .access(path.join(FOLDER_PROJECTS_TEST_PATH, project_name, 'DEVELOPMENT.md')) + .then(() => true) + .catch(() => false) + const isPlugin = ( + await fs.readFile(path.join(FOLDER_PROJECTS_TEST_PATH, project_name, 'config', 'robo.mjs')) + ).includes("type: 'plugin'") + const featureInstalled = areFeaturesInstalled(['prettier.config.mjs', '.eslintrc.json'], project_name) + + if (md_file_path && featureInstalled) { + expect(isPlugin).toBeTruthy() + } + }, 20000) + + it(`Standard TS Plugin: Same as Standard TS but as a plugin.`, async () => { + const project_name = 'TSTANDARDP' + await exec( + `node ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -rv 0.9.9 -ts --plugin -f prettier,eslint`, + true, + { + cwd: FOLDER_PROJECTS_TEST_PATH + } + ) + + const md_file_path = fs + .access(path.join(FOLDER_PROJECTS_TEST_PATH, project_name, 'DEVELOPMENT.md')) + .then(() => true) + .catch(() => false) + const isPlugin = ( + await fs.readFile(path.join(FOLDER_PROJECTS_TEST_PATH, project_name, 'config', 'robo.mjs')) + ).includes("type: 'plugin'") + const featureInstalled = areFeaturesInstalled(['prettier.config.mjs', '.eslintrc.json'], project_name) + + if (md_file_path && featureInstalled) { + expect(isPlugin).toBeTruthy() + } + }, 20000) + + // Package Managers + + /*it(`Standard TypeScript But PNPX: TypeScript, recommended features, and credentials.`, async () => { + const project_name = "TRFC" + await exec(`pnpx ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -ts -f prettier,eslint`, true, { + cwd: FOLDER_PROJECTS_TEST_PATH + }) + + const env_file_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, `.env`); + + const isCredentialsSet = await areCredentialsSet(env_file_path); + const featureInstalled = areFeaturesInstalled(["prettier.config.mjs", ".eslintrc.json"], project_name); + + if(featureInstalled){ + expect(isCredentialsSet).toBeTruthy(); + } + }, 20000) + + // yarn being different we gotta use "robo" and not "create-robo". + + it(`Standard TypeScript But Yarn create: TypeScript, recommended features, and credentials.`, async () => { + const project_name = "YTRFC"; + await exec(`yarn create ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -ts -f prettier,eslint`, true, { + cwd: FOLDER_PROJECTS_TEST_PATH + }) + + const env_file_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, `.env`); + + const isCredentialsSet = await areCredentialsSet(env_file_path); + const featureInstalled = areFeaturesInstalled(["prettier.config.mjs", ".eslintrc.json"], project_name); + + if(featureInstalled){ + expect(isCredentialsSet).toBeTruthy(); + } + }, 20000) + + // Bun X + + it(`Standard TypeScript But BUNX: TypeScript, recommended features, and credentials.`, async () => { + const project_name = "BXTRFC"; + await exec(`bunx ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -ts -f prettier,eslint`, true, { + cwd: FOLDER_PROJECTS_TEST_PATH + }) + + const env_file_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, `.env`); + + const isCredentialsSet = await areCredentialsSet(env_file_path); + const featureInstalled = areFeaturesInstalled(["prettier.config.mjs", ".eslintrc.json"], project_name); + + if(featureInstalled){ + expect(isCredentialsSet).toBeTruthy(); + } + }, 20000)*/ + + // Specials ! + + it(`Plugins: Same as Standard TS but with the api and ai plugins installed.`, async () => { + const project_name = 'STWAPIAI' + await exec( + `node ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -rv 0.9.9 -ts -f prettier,eslint --plugins @roboplay/plugin-ai @roboplay/plugin-api`, + true, + { + cwd: FOLDER_PROJECTS_TEST_PATH + } + ) + + const env_file_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, `.env`) + const isCredentialsSet = await areCredentialsSet(env_file_path) + + const featureInstalled = areFeaturesInstalled(['prettier.config.mjs', '.eslintrc.json'], project_name) + const pluginsInstalled = await arePluginsInstalled(['plugin-ai', 'plugin-api'], project_name) + + if (featureInstalled && isCredentialsSet) { + expect(pluginsInstalled).toBeTruthy() + } + }, 20000) + + it(`Robo Version: Same as Standard TS but using a specific version of Robo.js.`, async () => { + const project_name = 'STWV' + const rv = '0.9.0' + await exec(`node ${LOCAL_COPY_OF_ROBO_CREATE_PATH} ${project_name} -ts -rv ${rv} -f prettier,eslint`, true, { + cwd: FOLDER_PROJECTS_TEST_PATH + }) + + const env_file_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, `.env`) + const isCredentialsSet = await areCredentialsSet(env_file_path) + + const roboVersion = JSON.parse( + await fs.readFile(path.join(FOLDER_PROJECTS_TEST_PATH, project_name, 'package.json'), 'utf-8') + )['dependencies']['@roboplay/robo.js'] + const featuresInstalled = await areFeaturesInstalled(['prettier.config.mjs', '.eslintrc.json'], project_name) + + if (featuresInstalled && isCredentialsSet) { + expect(roboVersion).toBe(rv) + } + }, 20000) + + it(`No Install: Same as Standard TS minus the installation of dependencies.`, async () => { + const project_name = 'STMI' + await exec(`bunx create-robo ${project_name} -ts -f prettier,eslint -ni`, true, { + cwd: FOLDER_PROJECTS_TEST_PATH + }) + + const env_file_path = path.join(FOLDER_PROJECTS_TEST_PATH, project_name, `.env`) + + const isCredentialsSet = await areCredentialsSet(env_file_path) + const featureInstalled = areFeaturesInstalled(['prettier.config.mjs', '.eslintrc.json'], project_name) + const dependencies = JSON.parse( + await fs.readFile(path.join(FOLDER_PROJECTS_TEST_PATH, project_name, 'package.json'), 'utf-8') + )['dependencies'] + + if (featureInstalled && Object.keys(dependencies).length <= 0) { + expect(isCredentialsSet).toBeTruthy() + } + }, 20000) +}) + +function exec(command: string, passCreds: boolean, options?: SpawnOptions) { + return new Promise((resolve, reject) => { + // Run command as child process + const args = command.split(' ') + const childProcess = spawn(args.shift(), args, { + ...(options ?? {}), + env: { ...process.env, FORCE_COLOR: '1' }, + stdio: 'pipe' + }) + + childProcess.stdout.on('data', (data: any) => { + const convertString = String.fromCharCode(...data) + + if (convertString.includes('Client ID')) { + passCreds ? childProcess.stdin.write(`Credentials\n`) : childProcess.stdin.write(`\n`) + } + }) + + childProcess.stderr.on('data', function (data) { + console.log('stdout: ' + data) + }) + + // Resolve promise when child process exits + childProcess.on('close', (code) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`Child process exited with code ${code}`)) + } + }) + + // Or reject when it errors + childProcess.on('error', (error) => { + console.log(error) + reject(error) + }) + }) +} diff --git a/packages/create-robo/jest.config.mjs b/packages/create-robo/jest.config.mjs new file mode 100644 index 000000000..2bdd0e7b3 --- /dev/null +++ b/packages/create-robo/jest.config.mjs @@ -0,0 +1,7 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +export default { + preset: 'ts-jest', + resolver: 'jest-resolver-enhanced', + testEnvironment: 'node', + testMatch: ['**/__tests__/**/*.test.ts'] +} diff --git a/packages/create-robo/package.json b/packages/create-robo/package.json index e6b8423b3..32be8fd68 100644 --- a/packages/create-robo/package.json +++ b/packages/create-robo/package.json @@ -8,7 +8,8 @@ }, "scripts": { "build": "tsup", - "dev": "chokidar \"src/**/*.ts\" -d 1000 -c \"pnpm build\" --initial" + "dev": "chokidar \"src/**/*.ts\" -d 1000 -c \"pnpm build\" --initial", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "bin": { "create-robo": "dist/index.js" @@ -49,10 +50,15 @@ "@roboplay/robo.js": "0.9.0", "@types/async-retry": "^1.4.5", "@types/inquirer": "^9.0.3", + "@types/jest": "^29.5.5", + "@types/mocha": "^10.0.6", "@types/node": "^18.16.3", "@types/tar": "^6.1.4", "chokidar-cli": "^3.0.0", "discord.js": "^14.10.2", + "jest": "^29.7.0", + "jest-resolver-enhanced": "^1.1.0", + "ts-jest": "^29.1.1", "tsup": "6.7.0", "typescript": "5.0.2" } diff --git a/packages/create-robo/tsconfig.json b/packages/create-robo/tsconfig.json index dfc623773..39209a0c8 100644 --- a/packages/create-robo/tsconfig.json +++ b/packages/create-robo/tsconfig.json @@ -1,10 +1,10 @@ { - "include": ["src"], + "include": ["__tests__", "src"], "compilerOptions": { "target": "ESNext", "module": "node16", "lib": ["dom", "dom.iterable", "esnext"], - "types": ["node"], + "types": ["node", "jest"], "baseUrl": ".", "allowJs": true, "skipLibCheck": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a58d13e6..b901e7c8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,6 +64,12 @@ importers: '@types/inquirer': specifier: ^9.0.3 version: 9.0.3 + '@types/jest': + specifier: ^29.5.5 + version: 29.5.5 + '@types/mocha': + specifier: ^10.0.6 + version: 10.0.6 '@types/node': specifier: ^18.16.3 version: 18.16.8 @@ -76,6 +82,18 @@ importers: discord.js: specifier: ^14.10.2 version: 14.10.2 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.16.8) + jest-resolver-enhanced: + specifier: ^1.1.0 + version: 1.1.0 + mock-stdin: + specifier: ^1.0.0 + version: 1.0.0 + ts-jest: + specifier: ^29.1.1 + version: 29.1.1(@babel/core@7.23.2)(esbuild@0.17.18)(jest@29.7.0)(typescript@5.0.2) tsup: specifier: 6.7.0 version: 6.7.0(@swc/core@1.3.41)(typescript@5.0.2) @@ -2186,6 +2204,10 @@ packages: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: false + /@types/mocha@10.0.6: + resolution: {integrity: sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==} + dev: true + /@types/node@12.20.55: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: false @@ -3106,6 +3128,25 @@ packages: - ts-node dev: true + /create-jest@29.7.0(@types/node@18.16.8): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@18.16.8) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /cross-fetch@3.1.8: resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} dependencies: @@ -4603,6 +4644,34 @@ packages: - ts-node dev: true + /jest-cli@29.7.0(@types/node@18.16.8): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@18.16.8) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@18.16.8) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest-config@29.7.0(@types/node@18.15.5): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4993,6 +5062,27 @@ packages: - ts-node dev: true + /jest@29.7.0(@types/node@18.16.8): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@18.16.8) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -5381,6 +5471,10 @@ packages: engines: {node: '>=10'} hasBin: true + /mock-stdin@1.0.0: + resolution: {integrity: sha512-tukRdb9Beu27t6dN+XztSRHq9J0B/CoAOySGzHfn8UTfmqipA5yNT/sDUEyYdAV3Hpka6Wx6kOMxuObdOex60Q==} + dev: true + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -6456,7 +6550,7 @@ packages: bs-logger: 0.2.6 esbuild: 0.17.18 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@18.15.5) + jest: 29.7.0(@types/node@18.16.8) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2