diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d852f93364..c1fb46f3b7 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -60,7 +60,7 @@ jobs: uses: ./.github/actions/cache - name: Building ${{ matrix.client }} specs - run: yarn build:specs ${{ matrix.client }} + run: yarn cli build specs ${{ matrix.client }} - name: Check diff with pushed spec run: exit $(git status --porcelain specs/bundled/${{ matrix.client }}.yml | wc -l) @@ -123,7 +123,7 @@ jobs: - name: Generate ${{ matrix.client.name }} client if: steps.cache.outputs.cache-hit != 'true' - run: yarn generate javascript ${{ matrix.client.name }} + run: yarn cli generate javascript ${{ matrix.client.name }} - name: Check diff with pushed client if: steps.cache.outputs.cache-hit != 'true' @@ -131,7 +131,7 @@ jobs: - name: Build ${{ matrix.client.name }} client if: steps.cache.outputs.cache-hit != 'true' - run: yarn build:clients javascript ${{ matrix.client.name }} + run: yarn cli build clients javascript ${{ matrix.client.name }} client_java: runs-on: ubuntu-20.04 @@ -160,7 +160,7 @@ jobs: - name: Generate ${{ matrix.client.name }} client if: steps.cache.outputs.cache-hit != 'true' - run: yarn generate java ${{ matrix.client.name }} + run: yarn cli generate java ${{ matrix.client.name }} - name: Check diff with pushed client if: steps.cache.outputs.cache-hit != 'true' @@ -168,7 +168,7 @@ jobs: - name: Build ${{ matrix.client.name }} client if: steps.cache.outputs.cache-hit != 'true' - run: yarn build:clients java ${{ matrix.client.name }} + run: yarn cli build clients java ${{ matrix.client.name }} client_php: runs-on: ubuntu-20.04 @@ -190,7 +190,7 @@ jobs: - name: Generate ${{ matrix.client.name }} client if: steps.cache.outputs.cache-hit != 'true' - run: yarn generate php ${{ matrix.client.name }} + run: yarn cli generate php ${{ matrix.client.name }} - name: Check diff with pushed client if: steps.cache.outputs.cache-hit != 'true' @@ -198,7 +198,7 @@ jobs: - name: Build ${{ matrix.client.name }} client if: steps.cache.outputs.cache-hit != 'true' - run: yarn build:clients php ${{ matrix.client.name }} + run: yarn cli build clients php ${{ matrix.client.name }} cts: runs-on: ubuntu-20.04 @@ -217,20 +217,14 @@ jobs: with: job: cts - - name: Check script linting - run: yarn cts:lint:scripts - - - name: Test CTS script - run: yarn cts:test:scripts - - name: Generate CTS - run: yarn cts:generate + run: yarn cli cts generate - name: Check diff with pushed CTS run: exit $(git status --porcelain ./tests/output | wc -l) - name: Run CTS - run: yarn cts:test + run: yarn cli cts run scripts: runs-on: ubuntu-20.04 @@ -243,5 +237,8 @@ jobs: id: restore uses: ./.github/actions/cache + - name: Check script linting + run: yarn scripts:lint + - name: Test scripts - run: yarn workspace scripts test + run: yarn scripts:test diff --git a/README.md b/README.md index 344afbbb9c..5bdcc5c5a1 100644 --- a/README.md +++ b/README.md @@ -53,19 +53,19 @@ You can make changes locally and run commands through the docker container. #### Usage ```bash -yarn docker build:specs +yarn docker build specs ``` #### Build all specs ```bash -yarn docker build:specs +yarn docker build specs ``` #### Build specific spec ```bash -yarn docker build:specs recommend +yarn docker build specs recommend ``` #### Fix the specs format @@ -73,14 +73,14 @@ yarn docker build:specs recommend This is used by the build script and should not need to be called manually but if you want to format all specs file do: ```bash -yarn docker specs:fix +yarn specs:fix ``` If you just want to check the format (not override the files), run: ```bash -yarn docker specs:lint -yarn docker specs:lint search +yarn specs:lint +yarn specs:lint search ``` ### Generate clients based on the [`specs`](./specs/) @@ -102,15 +102,23 @@ yarn docker generate #### Usage ```bash -yarn docker build:clients +yarn docker build clients ``` ### Build specific client for specific language ```bash -yarn docker build:clients java recommend +yarn docker build clients java recommend ``` +### Verbose command + +You can add `-v` to almost every command to have a more verbose output. + +### Interactive command + +If you want to choose the language and client from a list you can add the `--interactive` option, or `-i`. + ## Testing clients You can test our generated clients by running: diff --git a/base.tsconfig.json b/base.tsconfig.json index e57897d3dc..4d0d503a84 100644 --- a/base.tsconfig.json +++ b/base.tsconfig.json @@ -17,5 +17,6 @@ "typeRoots": ["node_modules/@types"], "types": ["node"], "resolveJsonModule": true - } + }, + "exclude": ["node_modules"] } diff --git a/clients.config.json b/clients.config.json new file mode 100644 index 0000000000..817a6a11af --- /dev/null +++ b/clients.config.json @@ -0,0 +1,19 @@ +{ + "java": { + "folder": "clients/algoliasearch-client-java-2", + "tests": { + "extension": ".test.java", + "outputFolder": "src/test/java/com/algolia" + } + }, + "javascript": { + "folder": "clients/algoliasearch-client-javascript", + "tests": { + "extension": ".test.ts", + "outputFolder": "src" + } + }, + "php": { + "folder": "clients/algoliasearch-client-php" + } +} diff --git a/docs/CTS.md b/docs/CTS.md index 2d2c5badbf..3b3023a427 100644 --- a/docs/CTS.md +++ b/docs/CTS.md @@ -8,28 +8,28 @@ It is automaticaly generated for all languages, from a JSON entry point. > CTS requires all clients to be built ```bash -yarn docker build:specs -yarn docker build:clients -yarn docker cts:generate -yarn docker cts:test +yarn docker build specs all +yarn docker build clients all all +yarn docker cts generate all all +yarn docker cts run all ``` If you only want to generate the tests for a language, you can run: ```bash -yarn docker cts:generate javascript +yarn docker cts generate javascript all ``` Or for a specific client: ```bash -yarn docker cts:generate all search +yarn docker cts generate all search ``` Or a specific language and client: ```bash -yarn docker cts:generate javascript search +yarn docker cts generate javascript search ``` ## How to add test diff --git a/openapitools.json b/openapitools.json index 74a71578b0..c7f40ae154 100644 --- a/openapitools.json +++ b/openapitools.json @@ -241,7 +241,8 @@ "sourceFolder": "algoliasearch-core", "java8": true, "dateLibrary": "java8", - "packageName": "algoliasearch-client-java-2" + "packageName": "algoliasearch-client-java-2", + "packageVersion": "0.0.1" } }, "php-search": { @@ -259,7 +260,8 @@ "configClassname": "SearchConfig", "useCache": true, "variableNamingConvention": "camelCase", - "packageName": "algoliasearch-client-php" + "packageName": "algoliasearch-client-php", + "packageVersion": "0.0.1" } }, "php-recommend": { @@ -277,7 +279,8 @@ "configClassname": "RecommendConfig", "useCache": true, "variableNamingConvention": "camelCase", - "packageName": "algoliasearch-client-php" + "packageName": "algoliasearch-client-php", + "packageVersion": "0.0.1" } }, "php-personalization": { @@ -297,6 +300,7 @@ "allowedRegions": "us-eu", "variableNamingConvention": "camelCase", "packageName": "algoliasearch-client-php", + "packageVersion": "0.0.1", "isEuHost": true, "host": "personalization", "topLevelDomain": "com" @@ -319,6 +323,7 @@ "allowedRegions": "us-de", "variableNamingConvention": "camelCase", "packageName": "algoliasearch-client-php", + "packageVersion": "0.0.1", "fallbackToAliasHost": true, "isDeHost": true, "host": "analytics", @@ -342,6 +347,7 @@ "allowedRegions": "us-de", "variableNamingConvention": "camelCase", "packageName": "algoliasearch-client-php", + "packageVersion": "0.0.1", "fallbackToAliasHost": true, "isDeHost": true, "host": "insights", @@ -365,6 +371,7 @@ "allowedRegions": "us-de", "variableNamingConvention": "camelCase", "packageName": "algoliasearch-client-php", + "packageVersion": "0.0.1", "fallbackToAliasHost": true, "isDeHost": true, "host": "analytics", @@ -388,6 +395,7 @@ "allowedRegions": "us-eu", "variableNamingConvention": "camelCase", "packageName": "algoliasearch-client-php", + "packageVersion": "0.0.1", "isEuHost": true, "host": "query-suggestions", "topLevelDomain": "com" @@ -395,4 +403,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 4e3413016a..0d76fc72c0 100644 --- a/package.json +++ b/package.json @@ -5,32 +5,24 @@ "clients/algoliasearch-client-javascript/", "playground/javascript/node/", "playground/javascript/browser/", - "tests/", - "scripts/" + "scripts/", + "tests/output/javascript" ], "scripts": { - "build:clients": "./scripts/multiplexer.sh ${2:-nonverbose} ./scripts/builds/clients.sh ${0:-all} ${1:-all}", - "build:specs": "./scripts/builds/specs.sh ${0:-all} ${1:-yml}", - "build": "yarn build:specs && yarn build:clients", + "cli": "yarn workspace scripts ts-node --transpile-only ./index.ts", "clean": "rm -rf **/dist **/build **/node_modules **/.gradle", - "cts:generate": "yarn workspace tests build && ./scripts/multiplexer.sh ${2:-nonverbose} yarn workspace tests generate ${0:-all} ${1:-all}", - "cts:test": "./scripts/multiplexer.sh ${1:-nonverbose} ./scripts/runCTS.sh ${0:-all} all", - "cts:test:scripts": "yarn workspace tests test:scripts", - "cts:lint:scripts": "eslint --ext=ts tests/src/", "docker:build": "./scripts/docker/build.sh", "docker:clean": "docker stop dev; docker rm -f dev; docker image rm -f api-clients-automation", "docker:mount": "./scripts/docker/mount.sh", "docker:setup": "yarn docker:clean && yarn docker:build && yarn docker:mount", - "docker": "docker exec -it dev yarn $*", - "lint": "eslint --ext=ts .", - "post:generate": "./scripts/post-gen/global.sh", - "generate": "./scripts/multiplexer.sh ${2:-nonverbose} ./scripts/generate.sh ${0:-all} ${1:-all} && yarn post:generate ${0:-all}", + "docker": "docker exec -it dev yarn cli $*", + "github-actions:lint": "eslint --ext=yml .github/", "playground:browser": "yarn workspace javascript-browser-playground start", - "playground": "yarn && ./scripts/multiplexer.sh ${2:-nonverbose} ./scripts/playground.sh ${0:-javascript} ${1:-search}", + "release": "yarn workspace scripts createReleaseIssue", + "scripts:lint": "eslint --ext=ts scripts/", + "scripts:test": "yarn workspace scripts test", "specs:fix": "eslint --ext=yml specs/ --fix", - "specs:lint": "eslint --ext=yml specs/$0", - "github-actions:lint": "eslint --ext=yml .github/", - "release": "yarn workspace scripts createReleaseIssue" + "specs:lint": "eslint --ext=yml specs/$0" }, "devDependencies": { "@openapitools/openapi-generator-cli": "2.4.26", diff --git a/scripts/buildClients.ts b/scripts/buildClients.ts new file mode 100644 index 0000000000..082505e841 --- /dev/null +++ b/scripts/buildClients.ts @@ -0,0 +1,73 @@ +import { run } from './common'; +import { getLanguageFolder } from './config'; +import { createSpinner } from './oraLog'; +import type { Generator } from './types'; + +const multiBuildLanguage = new Set(['javascript']); + +/** + * Build only a specific client for one language, used by javascript for example. + */ +async function buildPerClient( + { language, key, additionalProperties: { packageName } }: Generator, + verbose: boolean +): Promise { + const spinner = createSpinner(`building ${key}`, verbose).start(); + switch (language) { + case 'javascript': + await run(`yarn workspace ${packageName} clean`, { verbose }); + await run( + `yarn workspace algoliasearch-client-javascript build ${packageName}`, + { verbose } + ); + break; + default: + } + spinner.succeed(); +} + +/** + * Build all client for a language at the same time, for those who live in the same folder. + */ +async function buildAllClients( + language: string, + verbose: boolean +): Promise { + const spinner = createSpinner(`building '${language}'`, verbose).start(); + switch (language) { + case 'java': + await run( + `./gradle/gradlew --no-daemon -p ${getLanguageFolder( + language + )} assemble`, + { + verbose, + } + ); + break; + case 'php': + break; + default: + } + spinner.succeed(); +} + +export async function buildClients( + generators: Generator[], + verbose: boolean +): Promise { + const langs = [...new Set(generators.map((gen) => gen.language))]; + + await Promise.all([ + Promise.all( + generators + .filter(({ language }) => multiBuildLanguage.has(language)) + .map((gen) => buildPerClient(gen, verbose)) + ), + Promise.all( + langs + .filter((lang) => !multiBuildLanguage.has(lang)) + .map((lang) => buildAllClients(lang, verbose)) + ), + ]); +} diff --git a/scripts/buildSpecs.ts b/scripts/buildSpecs.ts new file mode 100644 index 0000000000..44bf1e6208 --- /dev/null +++ b/scripts/buildSpecs.ts @@ -0,0 +1,71 @@ +import fsp from 'fs/promises'; + +import { hashElement } from 'folder-hash'; + +import { exists, run, toAbsolutePath } from './common'; +import { createSpinner } from './oraLog'; + +async function buildSpec( + client: string, + outputFormat: string, + verbose: boolean, + useCache: boolean +): Promise { + createSpinner(`'${client}' spec`, verbose).start().info(); + const cacheFile = toAbsolutePath(`specs/dist/${client}.cache`); + if (useCache) { + const spinner = createSpinner( + `checking cache for '${client}'`, + verbose + ).start(); + // check if file and cache exist + if (await exists(toAbsolutePath(`specs/bundled/${client}.yml`))) { + // compare with stored cache + const hash = (await hashElement(toAbsolutePath(`specs/${client}`))).hash; + if (await exists(cacheFile)) { + const storedHash = (await fsp.readFile(cacheFile)).toString(); + if (storedHash === hash) { + spinner.succeed( + `skipped building ${client} spec because the files did not change` + ); + return; + } + } + } + + spinner.info(`cache not found for ${client}' spec`); + } + + const spinner = createSpinner(`linting '${client}' spec`, verbose).start(); + await run(`yarn specs:lint ${client}`, { verbose }); + + spinner.text = `building '${client}' spec`; + await run( + `yarn openapi bundle specs/${client}/spec.yml -o specs/bundled/${client}.${outputFormat} --ext ${outputFormat}`, + { verbose } + ); + + spinner.text = `validating '${client}' spec`; + await run(`yarn openapi lint specs/bundled/${client}.${outputFormat}`, { + verbose, + }); + + spinner.text = `storing ${client} spec cache`; + const hash = (await hashElement(toAbsolutePath(`specs/${client}`))).hash; + await fsp.writeFile(cacheFile, hash); + + spinner.succeed(`building complete for '${client}' spec`); +} + +export async function buildSpecs( + clients: string[], + outputFormat: 'json' | 'yml', + verbose: boolean, + useCache: boolean +): Promise { + await fsp.mkdir(toAbsolutePath('specs/dist'), { recursive: true }); + + await Promise.all( + clients.map((client) => buildSpec(client, outputFormat, verbose, useCache)) + ); +} diff --git a/scripts/builds/clients.sh b/scripts/builds/clients.sh deleted file mode 100755 index 9d6e82491a..0000000000 --- a/scripts/builds/clients.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -# Break on non-zero code -set -e - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -# Move to the root (easier to locate other scripts) -cd ${DIR}/../.. - -LANGUAGE=$1 -CLIENT=$2 -GENERATOR="$LANGUAGE-$CLIENT" -PACKAGE=$(cat openapitools.json | jq -r --arg generator "$GENERATOR" '."generator-cli".generators[$generator].additionalProperties.packageName') - -if [[ -z $PACKAGE ]]; then - echo "Unknown package ${PACKAGE}" - exit 1 -fi - -# Commands are based on the LANGUAGE -if [[ $LANGUAGE == 'javascript' ]]; then - echo "> Cleaning previous build $GENERATOR..." - yarn workspace $PACKAGE clean - - echo "> Bundling $GENERATOR..." - CMD="yarn workspace algoliasearch-client-javascript build $PACKAGE" -elif [[ $LANGUAGE == 'php' ]]; then - # no build needed (for now) - : -elif [[ $LANGUAGE == 'java' ]]; then - CMD="./gradle/gradlew --no-daemon -p clients/$PACKAGE assemble" -fi - -if [[ $VERBOSE == "true" ]]; then - $CMD -else - set +e - log=$($CMD) - - if [[ $? != 0 ]]; then - echo "$log" - exit 1 - fi - set -e -fi diff --git a/scripts/builds/specs.sh b/scripts/builds/specs.sh deleted file mode 100755 index 3dd5bcfe94..0000000000 --- a/scripts/builds/specs.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash - -if [[ ! $CI ]] && [[ ! $DOCKER ]]; then - echo "You should run scripts via the docker container, see README.md" - - exit 1 -fi - -# Break on non-zero code -set -e - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -# Move to the root (easier to locate other scripts) -cd ${DIR}/../.. - -CLIENT=$1 -OUTPUT=$2 - -check_format_spec() { - local client=$1 - echo "> Checking format of $client spec" - yarn specs:lint $client - echo "" -} - -build_spec() { - local client=$1 - yarn openapi bundle specs/${client}/spec.yml -o specs/bundled/${client}.${OUTPUT} --ext ${OUTPUT} - echo "" -} - -validate_output_spec() { - local client=$1 - yarn openapi lint specs/bundled/${client}.${OUTPUT} - echo "" -} - -CLIENTS=($(find specs/*/spec.yml | awk -F / '{ print $(NF-1) }')) - -if [[ $CLIENT == "all" ]]; then - CLIENTS=("${CLIENTS[@]}") -elif [[ ${CLIENTS[*]} =~ ${CLIENT} ]]; then - CLIENTS=($CLIENT) -else - echo "Unknown spec ${CLIENT}" - exit 1 -fi - -if [[ $OUTPUT != "yml" ]] && [[ $OUTPUT != "json" ]]; then - echo "Unknown output ${OUTPUT}" - exit 1 -fi - -for client in "${CLIENTS[@]}"; do - check_format_spec $client - build_spec $client - validate_output_spec $client -done diff --git a/scripts/common.ts b/scripts/common.ts new file mode 100644 index 0000000000..41dcf7643d --- /dev/null +++ b/scripts/common.ts @@ -0,0 +1,94 @@ +import fsp from 'fs/promises'; +import path from 'path'; + +import execa from 'execa'; // https://github.com/sindresorhus/execa/tree/v5.1.1 + +import openapitools from '../openapitools.json'; + +import type { Generator, RunOptions } from './types'; + +export const CI = Boolean(process.env.CI); +export const DOCKER = Boolean(process.env.DOCKER); + +// This script is run by `yarn workspace ...`, which means the current working directory is `./script` +export const ROOT_DIR = path.resolve(process.cwd(), '..'); + +export const GENERATORS = Object.fromEntries( + Object.entries(openapitools['generator-cli'].generators).map(([key, gen]) => { + return [key, { ...gen, ...splitGeneratorKey(key) }]; + }) +); + +export const LANGUAGES = [ + ...new Set(Object.values(GENERATORS).map((gen) => gen.language)), +]; + +export const CLIENTS = [ + ...new Set(Object.values(GENERATORS).map((gen) => gen.client)), +]; + +export const CLIENTS_JS = CLIENTS.concat([]); + +/** + * Takes a generator key in the form 'language-client' and returns the Generator object. + */ +export function splitGeneratorKey(generatorKey: string): Generator { + const language = generatorKey.slice(0, generatorKey.indexOf('-')); + const client = generatorKey.slice(generatorKey.indexOf('-') + 1); + return { language, client, key: generatorKey }; +} + +export function createGeneratorKey({ + language, + client, +}: Pick): string { + return `${language}-${client}`; +} + +export async function run( + command: string, + { errorMessage, verbose, cwd = ROOT_DIR }: RunOptions = {} +): Promise { + try { + if (verbose) { + return ( + await execa.command(command, { + stdout: 'inherit', + shell: 'bash', + cwd, + }) + ).stdout; + } + return (await execa.command(command, { shell: 'bash', cwd })).stdout; + } catch (err) { + if (errorMessage) { + throw new Error(`[ERROR] ${errorMessage}`); + } else { + throw err; + } + } +} + +export async function exists(ppath: string): Promise { + try { + await fsp.stat(ppath); + return true; + } catch { + return false; + } +} + +export function toAbsolutePath(ppath: string): string { + return path.resolve(ROOT_DIR, ppath); +} + +export async function runIfExists( + scriptFile: string, + args: string, + opts: RunOptions = {} +): Promise { + if (await exists(toAbsolutePath(scriptFile))) { + return await run(`${scriptFile} ${args}`, opts); + } + return ''; +} diff --git a/scripts/config.ts b/scripts/config.ts new file mode 100644 index 0000000000..25baddcb41 --- /dev/null +++ b/scripts/config.ts @@ -0,0 +1,13 @@ +import clientsConfig from '../clients.config.json'; + +export function getLanguageFolder(language: string): string { + return clientsConfig[language].folder; +} + +export function getTestExtension(language: string): string | undefined { + return clientsConfig[language]?.tests?.extension; +} + +export function getTestOutputFolder(language: string): string | undefined { + return clientsConfig[language]?.tests?.outputFolder; +} diff --git a/tests/src/client/generate.ts b/scripts/cts/client/generate.ts similarity index 79% rename from tests/src/client/generate.ts rename to scripts/cts/client/generate.ts index 268c130564..20a74cbd95 100644 --- a/tests/src/client/generate.ts +++ b/scripts/cts/client/generate.ts @@ -2,12 +2,12 @@ import fsp from 'fs/promises'; import Mustache from 'mustache'; -import openapitools from '../../../openapitools.json'; +import { exists, toAbsolutePath } from '../../common'; +import { createSpinner } from '../../oraLog'; +import type { Generator } from '../../types'; import { walk, - packageNames, createClientName, - exists, createOutputDir, getOutputPath, loadTemplates, @@ -19,7 +19,7 @@ const testPath = 'client'; async function loadTests(client: string): Promise { const testsBlocks: TestsBlock[] = []; - const clientPath = `./CTS/client/${client}`; + const clientPath = toAbsolutePath(`tests/CTS/client/${client}`); if (!(await exists(clientPath))) { return []; @@ -57,19 +57,29 @@ async function loadTests(client: string): Promise { return testsBlocks; } -export async function generateTests( - language: string, - client: string +export async function generateClientTests( + { + language, + client, + additionalProperties: { hasRegionalHost, packageName }, + }: Generator, + verbose: boolean ): Promise { + let spinner = createSpinner( + { text: 'generating client tests', indent: 4 }, + verbose + ).start(); const testsBlocks = await loadTests(client); if (testsBlocks.length === 0) { - // eslint-disable-next-line no-console - console.warn( - `Skipping because tests dont't exist for CTS > generate:client for ${language}-${client}` - ); + spinner.warn("skipping because tests doesn't exist"); return; } + spinner.info(); + spinner = createSpinner( + { text: 'loading templates', indent: 8 }, + verbose + ).start(); await createOutputDir({ language, testPath }); @@ -79,28 +89,24 @@ export async function generateTests( }); if (!template) { - // eslint-disable-next-line no-console - console.warn( - `Skipping because template doesn't exist for CTS > generate:client for ${language}-${client}` - ); + spinner.warn("skipping because template doesn't exist"); return; } + spinner.text = 'rendering templates'; const code = Mustache.render( template, { - import: packageNames[language][client], + import: packageName, client: createClientName(client, language), blocks: modifyForMustache(testsBlocks), - hasRegionalHost: openapitools['generator-cli'].generators[ - `${language}-${client}` - ].additionalProperties.hasRegionalHost - ? true - : undefined, + hasRegionalHost: hasRegionalHost ? true : undefined, }, partialTemplates ); await fsp.writeFile(getOutputPath({ language, client, testPath }), code); + + spinner.succeed(); } function serializeParameters(parameters: any): string { diff --git a/tests/src/client/types.ts b/scripts/cts/client/types.ts similarity index 100% rename from tests/src/client/types.ts rename to scripts/cts/client/types.ts diff --git a/scripts/cts/generate.ts b/scripts/cts/generate.ts new file mode 100644 index 0000000000..e2b416835c --- /dev/null +++ b/scripts/cts/generate.ts @@ -0,0 +1,47 @@ +import { toAbsolutePath } from '../common'; +import { getTestOutputFolder } from '../config'; +import { formatter } from '../formatter'; +import { createSpinner } from '../oraLog'; +import type { Generator } from '../types'; + +import { generateClientTests } from './client/generate'; +import { generateRequestsTests } from './methods/requests/generate'; + +async function ctsGenerate( + generator: Generator, + verbose: boolean +): Promise { + createSpinner(`generating CTS for ${generator.key}`, verbose).start().info(); + switch (generator.language) { + case 'javascript': + case 'java': + await generateRequestsTests(generator, verbose); + await generateClientTests(generator, verbose); + break; + default: + } +} + +export async function ctsGenerateMany( + generators: Generator[], + verbose: boolean +): Promise { + for (const gen of generators) { + if (!getTestOutputFolder(gen.language)) { + continue; + } + await ctsGenerate(gen, verbose); + } + + const langs = [...new Set(generators.map((gen) => gen.language))]; + for (const lang of langs) { + if (!getTestOutputFolder(lang)) { + continue; + } + await formatter( + lang, + toAbsolutePath(`tests/output/${lang}/${getTestOutputFolder(lang)}`), + verbose + ); + } +} diff --git a/tests/src/integrations/.gitkeep b/scripts/cts/integrations/.gitkeep similarity index 100% rename from tests/src/integrations/.gitkeep rename to scripts/cts/integrations/.gitkeep diff --git a/tests/src/methods/requests/cts.ts b/scripts/cts/methods/requests/cts.ts similarity index 89% rename from tests/src/methods/requests/cts.ts rename to scripts/cts/methods/requests/cts.ts index 0b79e23e70..06e9e0599d 100644 --- a/tests/src/methods/requests/cts.ts +++ b/scripts/cts/methods/requests/cts.ts @@ -3,10 +3,10 @@ import fsp from 'fs/promises'; import SwaggerParser from '@apidevtools/swagger-parser'; import type { OpenAPIV3 } from 'openapi-types'; +import { exists, toAbsolutePath } from '../../../common'; import { removeEnumType, removeObjectName, walk } from '../../utils'; import type { - CTS, CTSBlock, ParametersWithDataType, RequestCTS, @@ -93,14 +93,6 @@ function transformParam({ } return out; } - - if (!objectName) { - // throw new Error(`Object ${key} missing property $objectName in test ${testName}`); - // eslint-disable-next-line no-console - console.log( - `Object ${key} missing property $objectName in test ${testName}` - ); - } } else if (isArray) { // recursive on all value out = value.map((v, i) => @@ -141,12 +133,18 @@ function createParamWithDataType({ return [transformed]; } -async function loadRequestsCTS(client: string): Promise { +export async function loadRequestsCTS(client: string): Promise { // load the list of operations from the spec - const spec = await SwaggerParser.validate(`../specs/${client}/spec.yml`); + const spec = await SwaggerParser.validate( + toAbsolutePath(`specs/${client}/spec.yml`) + ); if (!spec.paths) { throw new Error(`No paths found for spec ${client}/spec.yml`); } + if (!(await exists(toAbsolutePath(`tests/CTS/methods/requests/${client}`)))) { + // skip it if no CTS for this client + return []; + } const operations = Object.values(spec.paths) .flatMap((p) => Object.values(p)) @@ -154,7 +152,9 @@ async function loadRequestsCTS(client: string): Promise { const ctsClient: CTSBlock[] = []; - for await (const file of walk(`./CTS/methods/requests/${client}`)) { + for await (const file of walk( + toAbsolutePath(`tests/CTS/methods/requests/${client}`) + )) { if (!file.name.endsWith('json')) { continue; } @@ -223,9 +223,3 @@ async function loadRequestsCTS(client: string): Promise { t1.operationId.localeCompare(t2.operationId) ); } - -export async function loadCTS(client: string): Promise { - return { - requests: await loadRequestsCTS(client), - }; -} diff --git a/scripts/cts/methods/requests/generate.ts b/scripts/cts/methods/requests/generate.ts new file mode 100644 index 0000000000..9cb1591375 --- /dev/null +++ b/scripts/cts/methods/requests/generate.ts @@ -0,0 +1,73 @@ +import fsp from 'fs/promises'; + +import Mustache from 'mustache'; + +import { createSpinner } from '../../../oraLog'; +import type { Generator } from '../../../types'; +import { + createClientName, + capitalize, + getOutputPath, + createOutputDir, + loadTemplates, +} from '../../utils'; + +import { loadRequestsCTS } from './cts'; + +const testPath = 'methods/requests'; + +export async function generateRequestsTests( + { + language, + client, + additionalProperties: { hasRegionalHost, packageName }, + }: Generator, + verbose: boolean +): Promise { + createSpinner({ text: 'generating requests tests', indent: 4 }, verbose) + .start() + .info(); + const spinner = createSpinner( + { text: 'loading templates', indent: 8 }, + verbose + ).start(); + const { requests: template, ...partialTemplates } = await loadTemplates({ + language, + testPath, + }); + + spinner.text = 'loading CTS'; + const cts = await loadRequestsCTS(client); + + if (cts.length === 0) { + spinner.warn("skipping because tests doesn't exist"); + return; + } + + await createOutputDir({ language, testPath }); + + spinner.text = 'rendering templates'; + const code = Mustache.render( + template, + { + import: packageName, + client: createClientName(client, language), + blocks: cts, + hasRegionalHost: hasRegionalHost ? true : undefined, + capitalize() { + return function (text: string, render: (t: string) => string): string { + return capitalize(render(text)); + }; + }, + escapeQuotes() { + return function (text: string, render: (t: string) => string): string { + return render(text).replace(/"/g, '\\"'); + }; + }, + }, + partialTemplates + ); + + await fsp.writeFile(getOutputPath({ language, client, testPath }), code); + spinner.succeed(); +} diff --git a/tests/src/methods/requests/types.ts b/scripts/cts/methods/requests/types.ts similarity index 100% rename from tests/src/methods/requests/types.ts rename to scripts/cts/methods/requests/types.ts diff --git a/scripts/cts/runCts.ts b/scripts/cts/runCts.ts new file mode 100644 index 0000000000..9b637cc57d --- /dev/null +++ b/scripts/cts/runCts.ts @@ -0,0 +1,42 @@ +import { run } from '../common'; +import { createSpinner } from '../oraLog'; + +async function runCtsOne(language: string, verbose: boolean): Promise { + const spinner = createSpinner( + `running cts for '${language}'`, + verbose + ).start(); + switch (language) { + case 'javascript': + await run('yarn workspace javascript-tests test', { verbose }); + break; + case 'java': + await run('./gradle/gradlew --no-daemon -p tests/output/java test', { + verbose, + }); + break; + /* not working yet + case 'php': { + let php = 'php8'; + if (CI) php = 'php'; + await run( + `${php} ./clients/algoliasearch-client-php/vendor/bin/phpunit tests/output/php`, + { verbose } + ); + break; + }*/ + default: + spinner.warn(`skipping unknown language '${language}' to run the CTS`); + return; + } + spinner.succeed(); +} + +export async function runCts( + languages: string[], + verbose: boolean +): Promise { + for (const lang of languages) { + await runCtsOne(lang, verbose); + } +} diff --git a/tests/src/utils.test.ts b/scripts/cts/utils.test.ts similarity index 89% rename from tests/src/utils.test.ts rename to scripts/cts/utils.test.ts index de0f60ea20..dc1401bb46 100644 --- a/tests/src/utils.test.ts +++ b/scripts/cts/utils.test.ts @@ -1,6 +1,5 @@ import { capitalize, - checkIfLanguageExists, createClientName, removeEnumType, removeObjectName, @@ -116,14 +115,4 @@ describe('utils', () => { }); }); }); - - describe('checkIfLanguageExists', () => { - it('returns `true` if the language is present in the config', () => { - expect(checkIfLanguageExists('javascript')).toBe(true); - }); - - it('returns `false` if the language is not present in the config', () => { - expect(checkIfLanguageExists('algo')).toBe(false); - }); - }); }); diff --git a/tests/src/utils.ts b/scripts/cts/utils.ts similarity index 54% rename from tests/src/utils.ts rename to scripts/cts/utils.ts index a6fd563a2d..e6ace617ab 100644 --- a/tests/src/utils.ts +++ b/scripts/cts/utils.ts @@ -1,30 +1,8 @@ import fsp from 'fs/promises'; import path from 'path'; -import openapitools from '../../openapitools.json'; -import ctsConfig from '../CTS/config.json'; - -// For each generator, we map the packageName with the language and client -export const packageNames: Record< - string, - Record -> = Object.entries(openapitools['generator-cli'].generators).reduce( - (prev, [clientName, clientConfig]) => { - const obj = prev; - const parts = clientName.split('-'); - const lang = parts[0]; - const client = parts.slice(1).join('-'); - - if (!(lang in prev)) { - obj[lang] = {}; - } - - obj[lang][client] = clientConfig.additionalProperties.packageName; - - return obj; - }, - {} as Record> -); +import { exists, toAbsolutePath } from '../common'; +import { getTestExtension, getTestOutputFolder } from '../config'; export async function* walk( dir: string @@ -36,15 +14,6 @@ export async function* walk( } } -export async function exists(filePath: string): Promise { - try { - await fsp.stat(filePath); - return true; - } catch (err) { - return false; - } -} - export function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1); } @@ -84,10 +53,6 @@ export function removeObjectName(obj: any): any { return obj; } -export function checkIfLanguageExists(language: string): boolean { - return Boolean(ctsConfig[language]); -} - export function removeEnumType(obj: any): any { if (typeof obj === 'object') { if (Array.isArray(obj)) { @@ -103,43 +68,6 @@ export function removeEnumType(obj: any): any { return obj; } -function printUsage(commandName: string): void { - /* eslint-disable no-console */ - console.log(`usage: ${commandName} language client`); - // eslint-disable-next-line no-process-exit - process.exit(1); -} - -export function parseCLI( - args: string[], - commandName: string -): { lang: string; client: string } { - if (args.length < 3) { - console.log('not enough arguments'); - printUsage(commandName); - } - - const lang = args[2]; - const client = args[3]; - - if (!(lang in packageNames)) { - console.log('Unknown language', lang); - // eslint-disable-next-line no-process-exit - process.exit(1); - } - if (!(client in packageNames[lang])) { - console.log('Unknown client', client); - // eslint-disable-next-line no-process-exit - process.exit(1); - } - /* eslint-enable no-console */ - - return { - lang, - client, - }; -} - export async function createOutputDir({ language, testPath, @@ -148,7 +76,9 @@ export async function createOutputDir({ testPath: string; }): Promise { await fsp.mkdir( - `output/${language}/${ctsConfig[language].outputFolder}/${testPath}`, + toAbsolutePath( + `tests/output/${language}/${getTestOutputFolder(language)}/${testPath}` + ), { recursive: true, } @@ -164,7 +94,11 @@ export function getOutputPath({ client: string; testPath: string; }): string { - return `output/${language}/${ctsConfig[language].outputFolder}/${testPath}/${client}.${ctsConfig[language].extension}`; + return toAbsolutePath( + `tests/output/${language}/${getTestOutputFolder( + language + )}/${testPath}/${client}${getTestExtension(language)}` + ); } export async function loadTemplates({ @@ -175,7 +109,9 @@ export async function loadTemplates({ testPath: string; }): Promise> { const templates: Record = {}; - const templatePath = `./CTS/${testPath}/templates/${language}`; + const templatePath = toAbsolutePath( + `tests/CTS/${testPath}/templates/${language}` + ); if (!(await exists(templatePath))) { return {}; diff --git a/scripts/formatter.sh b/scripts/formatter.sh deleted file mode 100755 index 0aa4024e3c..0000000000 --- a/scripts/formatter.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -# Break on non-zero code -set -e - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -# Move to the root (easier to locate other scripts) -cd ${DIR}/.. - -LANGUAGE=$1 -FOLDER=$2 - -if [[ ! -d "$FOLDER" ]]; then - echo "Output folder does not exist for $LANGUAGE, skipping..." - exit 0 -fi - -if [[ $LANGUAGE == 'javascript' ]]; then - # jsdoc/require-hyphen-before-param-description fails to lint more than - # 6 parameters, we re-run the script if failed to lint the rest - CMD="yarn eslint --ext=ts,js ${FOLDER} --fix || yarn eslint --ext=ts,js ${FOLDER} --fix" -elif [[ $LANGUAGE == 'php' ]]; then - FIXER="clients/algoliasearch-client-php/vendor/bin/php-cs-fixer" - if [[ $CI ]]; then - PHP="php" - else - PHP="php8" - fi - CMD="composer update --working-dir=clients/algoliasearch-client-php \ - && composer dump-autoload --working-dir=clients/algoliasearch-client-php \ - && PHP_CS_FIXER_IGNORE_ENV=1 $PHP $FIXER fix $FOLDER --using-cache=no --allow-risky=yes" -elif [[ $LANGUAGE == 'java' ]]; then - CMD="find $FOLDER -type f -name \"*.java\" | xargs java --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ - -jar /tmp/java-formatter.jar -r \ - && yarn prettier --write $FOLDER" -else - echo "Cannot format unknow language $LANGUAGE" - exit 1 -fi - -echo "> Formatting ${LANGUAGE} in ${FOLDER}..." - -if [[ $VERBOSE == "true" ]]; then - # CAREFUL WITH EVAL (not safe) - eval $CMD -else - set +e - log=$(eval $CMD) - - if [[ $? != 0 ]]; then - echo "$log" - exit 1 - fi - set -e -fi diff --git a/scripts/formatter.ts b/scripts/formatter.ts new file mode 100644 index 0000000000..b35a5918f5 --- /dev/null +++ b/scripts/formatter.ts @@ -0,0 +1,39 @@ +import { CI, run } from './common'; +import { createSpinner } from './oraLog'; + +export async function formatter( + language: string, + folder: string, + verbose = false +): Promise { + const spinner = createSpinner( + { text: `formatting '${language}'`, indent: 4 }, + verbose + ).start(); + let cmd = ''; + switch (language) { + case 'javascript': + cmd = `yarn eslint --ext=ts,js ${folder} --fix || yarn eslint --ext=ts,js ${folder} --fix`; + break; + case 'java': + cmd = `find ${folder} -type f -name "*.java" | xargs java --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ + -jar /tmp/java-formatter.jar -r \ + && yarn prettier --write ${folder}`; + break; + case 'php': + cmd = `composer update --working-dir=clients/algoliasearch-client-php \ + && composer dump-autoload --working-dir=clients/algoliasearch-client-php \ + && PHP_CS_FIXER_IGNORE_ENV=1 ${ + CI ? 'php' : 'php8' + } clients/algoliasearch-client-php/vendor/bin/php-cs-fixer fix ${folder} --using-cache=no --allow-risky=yes`; + break; + default: + return; + } + await run(cmd, { verbose }); + spinner.succeed(); +} diff --git a/scripts/generate.sh b/scripts/generate.sh deleted file mode 100755 index bf25a6314a..0000000000 --- a/scripts/generate.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/bin/bash - -# Break on non-zero code -set -e - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -# Move to the root (easier to locate other scripts) -cd ${DIR}/.. - -LANGUAGE=$1 -CLIENT=$2 -GENERATOR="$1-$2" - -compute_hash() { - cacheSpec=$(find specs/$CLIENT -type f -print0 | xargs -0 sha1sum | sha1sum | tr -d ' ') - cacheCommon=$(find specs/common -type f -print0 | xargs -0 sha1sum | sha1sum | tr -d ' ') - echo "$cacheSpec$cacheCommon" -} - -# build spec before generating client -build_spec() { - # check if file and cache exist - mkdir -p specs/dist - cacheFile="specs/dist/$CLIENT.cache" - if [[ -f specs/bundled/$CLIENT.yml ]]; then - cache=$(compute_hash) - # compare with stored cache - if [[ -f $cacheFile && $(cat $cacheFile) == $cache ]]; then - echo "> Skipped building spec because the files did not change..." - return - fi - fi - yarn build:specs $CLIENT - - # store hash - cache=$(compute_hash) - echo $cache > $cacheFile -} - -# Run the pre generation script if it exists. -run_pre_gen() { - pregen="./scripts/pre-gen/${LANGUAGE}.sh" - - if [[ -f "$pregen" ]]; then - echo "> Running pre-gen script for $GENERATOR..." - $pregen $CLIENT - fi - - # Sets the hosts option to the `openapitools.json` config file - yarn workspace scripts setHostsOptions $LANGUAGE $CLIENT -} - -generate_client() { - echo "> Generating code for $GENERATOR..." - - CMD="yarn openapi-generator-cli generate --generator-key $GENERATOR" - - if [[ $VERBOSE == "true" ]]; then - $CMD - else - set +e - log=$($CMD) - - if [[ $? != 0 ]]; then - echo "$log" - exit 1 - fi - set -e - fi -} - -# Run the post generation script if it exists. -run_post_gen() { - postgen="./scripts/post-gen/${LANGUAGE}.sh" - - folder=$(cat openapitools.json | jq -r --arg generator "$GENERATOR" '."generator-cli".generators[$generator].output' | sed 's/#{cwd}\///g') - - if [[ -f "$postgen" ]]; then - echo "> Running post-gen script for $GENERATOR..." - $postgen $folder $GENERATOR - fi - - ./scripts/formatter.sh $LANGUAGE $folder -} - -if [[ ! $CI ]]; then - build_spec -fi - -run_pre_gen -generate_client -run_post_gen diff --git a/scripts/generate.ts b/scripts/generate.ts new file mode 100644 index 0000000000..5a6d5a594c --- /dev/null +++ b/scripts/generate.ts @@ -0,0 +1,94 @@ +import { buildSpecs } from './buildSpecs'; +import { CI, run, runIfExists } from './common'; +import { getLanguageFolder } from './config'; +import { formatter } from './formatter'; +import { createSpinner } from './oraLog'; +import { setHostsOptions } from './pre-gen/setHostsOptions'; +import type { Generator } from './types'; + +async function preGen( + { language, client, key, output }: Generator, + verbose?: boolean +): Promise { + const folder = output.replace('#{cwd}/', ''); + await runIfExists(`./scripts/pre-gen/${language}.sh`, `${folder} ${key}`, { + verbose, + }); + + await setHostsOptions({ client, key }); +} + +async function generateClient( + { key }: Generator, + verbose?: boolean +): Promise { + await run(`yarn openapi-generator-cli generate --generator-key ${key}`, { + verbose, + }); +} + +async function postGen( + { language, key, output }: Generator, + verbose?: boolean +): Promise { + const folder = output.replace('#{cwd}/', ''); + await runIfExists(`./scripts/post-gen/${language}.sh`, `${folder} ${key}`, { + verbose, + }); +} + +export async function generate( + generators: Generator[], + verbose: boolean +): Promise { + if (!CI) { + const clients = [...new Set(generators.map((gen) => gen.client))]; + await buildSpecs(clients, 'yml', verbose, true); + } + + for (const gen of generators) { + const spinner = createSpinner(`pre-gen ${gen.key}`, verbose).start(); + await preGen(gen, verbose); + + spinner.text = `generating ${gen.key}`; + await generateClient(gen, verbose); + + spinner.text = `post-gen ${gen.key}`; + await postGen(gen, verbose); + + if (gen.language === 'javascript' && CI) { + // because the CI is parallelized, run the formatter for each client + await formatter(gen.language, gen.output, verbose); + } + + spinner.succeed(); + } + + const langs = [...new Set(generators.map((gen) => gen.language))]; + for (const lang of langs) { + if (!CI || lang !== 'javascript') { + await formatter(lang, getLanguageFolder(lang), verbose); + } + if (lang === 'javascript') { + const spinner = createSpinner( + 'cleaning JavaScript client utils', + verbose + ).start(); + await run('yarn workspace algoliasearch-client-javascript clean:utils', { + verbose, + }); + spinner.text = 'building JavaScript client utils'; + await run('yarn workspace algoliasearch-client-javascript build:utils', { + verbose, + }); + + spinner.succeed(); + } + } + + if (!CI) { + const spinner = createSpinner('formatting specs', verbose).start(); + await run(`yarn specs:fix`, { verbose }); + spinner.succeed(); + } +} diff --git a/scripts/index.ts b/scripts/index.ts new file mode 100644 index 0000000000..8ccae24a1a --- /dev/null +++ b/scripts/index.ts @@ -0,0 +1,269 @@ +/* eslint-disable no-param-reassign */ +import { Argument, program } from 'commander'; +import inquirer from 'inquirer'; + +import { buildClients } from './buildClients'; +import { buildSpecs } from './buildSpecs'; +import { + CI, + CLIENTS, + CLIENTS_JS, + createGeneratorKey, + DOCKER, + GENERATORS, + LANGUAGES, +} from './common'; +import { ctsGenerateMany } from './cts/generate'; +import { runCts } from './cts/runCts'; +import { generate } from './generate'; +import { playground } from './playground'; +import type { Generator } from './types'; + +if (!CI && !DOCKER) { + // eslint-disable-next-line no-console + console.log('You should run scripts via the docker container, see README.md'); + // eslint-disable-next-line no-process-exit + process.exit(1); +} + +program.name('cli'); + +async function promptLanguage( + defaut: string | undefined, + interactive: boolean +): Promise { + if (defaut) { + return defaut; + } + if (!interactive) { + return 'all'; + } + const { language } = await inquirer.prompt([ + { + type: 'list', + name: 'language', + message: 'Select a language', + default: 'all', + choices: ['all', new inquirer.Separator()].concat(LANGUAGES), + }, + ]); + return language; +} + +async function promptClient( + defaut: string | undefined, + interactive: boolean, + clientList = CLIENTS +): Promise { + if (defaut) { + return defaut; + } + if (!interactive) { + return 'all'; + } + const { client } = await inquirer.prompt([ + { + type: 'list', + name: 'client', + message: 'Select a client', + default: 'all', + choices: ['all', new inquirer.Separator()].concat(clientList), + }, + ]); + return client; +} + +function generatorList({ + language, + client, + clientList = CLIENTS, +}: Pick & { + clientList?: string[]; +}): Generator[] { + let langsTodo = [language]; + let clientsTodo = [client]; + if (language === 'all') { + langsTodo = LANGUAGES; + } + if (client === 'all') { + clientsTodo = clientList; + } + + return langsTodo + .flatMap((lang) => + clientsTodo.map( + (cli) => GENERATORS[createGeneratorKey({ language: lang, client: cli })] + ) + ) + .filter(Boolean); +} + +program + .command('generate') + .description('Generate a specified client') + .addArgument( + new Argument('[language]', 'The language').choices( + ['all'].concat(LANGUAGES) + ) + ) + .addArgument( + new Argument('[client]', 'The client').choices(['all'].concat(CLIENTS)) + ) + .option('-v, --verbose', 'make the generation verbose') + .option('-i, --interactive', 'open prompt to query parameters') + .action( + async ( + language: string | undefined, + client: string | undefined, + { verbose, interactive } + ) => { + language = await promptLanguage(language, interactive); + client = await promptClient(client, interactive); + + await generate(generatorList({ language, client }), Boolean(verbose)); + } + ); + +const buildCommand = program.command('build'); + +buildCommand + .command('clients') + .description('Build a specified client') + .addArgument( + new Argument('[language]', 'The language').choices( + ['all'].concat(LANGUAGES) + ) + ) + .addArgument( + new Argument('[client]', 'The client').choices(['all'].concat(CLIENTS_JS)) + ) + .option('-v, --verbose', 'make the compilation verbose') + .option('-i, --interactive', 'open prompt to query parameters') + .action( + async ( + language: string | undefined, + client: string | undefined, + { verbose, interactive } + ) => { + language = await promptLanguage(language, interactive); + client = await promptClient(client, interactive, CLIENTS_JS); + + await buildClients( + generatorList({ language, client, clientList: CLIENTS_JS }), + Boolean(verbose) + ); + } + ); + +buildCommand + .command('specs') + .description('Build a specified spec') + .addArgument( + new Argument('[client]', 'The client').choices(['all'].concat(CLIENTS)) + ) + .addArgument( + new Argument('[output-format]', 'The output format').choices([ + 'yml', + 'json', + ]) + ) + .option('-v, --verbose', 'make the verification verbose') + .option('-i, --interactive', 'open prompt to query parameters') + .action( + async ( + client: string | undefined, + outputFormat: 'json' | 'yml' | undefined, + { verbose, interactive } + ) => { + client = await promptClient(client, interactive); + + if (!outputFormat) { + outputFormat = 'yml'; + } + + let clientsTodo = [client]; + if (client === 'all') { + clientsTodo = CLIENTS; + } + // ignore cache when building from cli + await buildSpecs(clientsTodo, outputFormat!, Boolean(verbose), false); + } + ); + +const ctsCommand = program.command('cts'); + +ctsCommand + .command('generate') + .description('Generate the CTS tests') + .addArgument( + new Argument('[language]', 'The language').choices( + ['all'].concat(LANGUAGES) + ) + ) + .addArgument( + new Argument('[client]', 'The client').choices(['all'].concat(CLIENTS)) + ) + .option('-v, --verbose', 'make the generation verbose') + .option('-i, --interactive', 'open prompt to query parameters') + .action( + async ( + language: string | undefined, + client: string | undefined, + { verbose, interactive } + ) => { + language = await promptLanguage(language, interactive); + client = await promptClient(client, interactive); + + await ctsGenerateMany( + generatorList({ language, client }), + Boolean(verbose) + ); + } + ); + +ctsCommand + .command('run') + .description('Run the tests for the CTS') + .addArgument( + new Argument('[language]', 'The language').choices( + ['all'].concat(LANGUAGES) + ) + ) + .option('-v, --verbose', 'make the generation verbose') + .option('-i, --interactive', 'open prompt to query parameters') + .action(async (language: string | undefined, { verbose, interactive }) => { + language = await promptLanguage(language, interactive); + + let langsTodo = [language]; + if (language === 'all') { + langsTodo = LANGUAGES; + } + await runCts(langsTodo, Boolean(verbose)); + }); + +program + .command('playground') + .description('Run the playground') + .addArgument(new Argument('[language]', 'The language').choices(LANGUAGES)) + .addArgument( + new Argument('[client]', 'The client').choices(['all'].concat(CLIENTS)) + ) + .option('-i, --interactive', 'open prompt to query parameters') + .action( + async ( + language: string | undefined, + client: string | undefined, + { interactive } + ) => { + language = await promptLanguage(language, interactive); + client = await promptClient(client, interactive); + + await playground({ + language, + client, + key: createGeneratorKey({ language, client }), + }); + } + ); + +program.parse(); diff --git a/scripts/multiplexer.sh b/scripts/multiplexer.sh deleted file mode 100755 index 48cc0b561b..0000000000 --- a/scripts/multiplexer.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash -# Call this script with multiplexer.sh -# to run the cmd for all the required lang-client combination - -if [[ ! $CI ]] && [[ ! $DOCKER ]]; then - echo "You should run scripts via the docker container, see README.md" - - exit 1 -fi - -# Break on non-zero code -set -e - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -# Move to the root (easier to locate other scripts) -cd ${DIR}/.. - -CMD=${@:2:$#-3} # all arguments but the last 2 and first one -LANGUAGE=${@: -2:1} # before last argument -CLIENT=${@: -1} # last argument - -if [[ $1 == "verbose" ]]; then - export VERBOSE=true - echo "Verbose mode" -fi - -if [[ $CMD == "./scripts/playground.sh" ]] && ([[ $LANGUAGE == "all" ]] || [[ $CLIENT == "all" ]]); then - echo "You cannot use 'all' when running the playground, please specify the language and client" - - exit 1 -fi - -if [[ $CMD == "./scripts/runCTS.sh" ]]; then - if [[ $CLIENT == "all" ]]; then - CLIENT=search # dummy client to only run once - else - echo "You must use 'all' clients when testing the CTS, as they all run at the same time" - - exit 1 - fi -fi - -LANGUAGES=() -CLIENTS=() - -GENERATORS=() - -find_clients_and_languages() { - echo "> Searching for available languages and clients..." - - GENERATORS=( $(cat openapitools.json | jq '."generator-cli".generators' | jq -r 'keys[]') ) - - for generator in "${GENERATORS[@]}"; do - local lang=${generator%%-*} - local client=${generator#*-} - - if [[ ! ${LANGUAGES[*]} =~ $lang ]]; then - LANGUAGES+=($lang) - fi - - if [[ ! ${CLIENTS[*]} =~ $client ]]; then - CLIENTS+=($client) - fi - done -} - -find_clients_and_languages - -if [[ $LANGUAGE == "all" ]]; then - LANGUAGE=("${LANGUAGES[@]}") -elif [[ " ${LANGUAGES[*]} " =~ " ${LANGUAGE} " ]]; then - LANGUAGE=($LANGUAGE) -else - echo "Unknown language ${LANGUAGE}" - exit 1 -fi - -if [[ $CLIENT == "all" ]]; then - CLIENT=("${CLIENTS[@]}") -elif [[ " ${CLIENTS[*]} " =~ " ${CLIENT} " ]]; then - CLIENT=($CLIENT) -else - echo "Unknown client ${CLIENT}" - exit 1 -fi - -for lang in "${LANGUAGE[@]}"; do - for client in "${CLIENT[@]}"; do - if [[ " ${GENERATORS[*]} " =~ " ${lang}-${client} " ]]; then - $CMD $lang $client - echo "" - fi - done -done - -# Format after every client for the CTS -if [[ $CMD == 'yarn workspace tests generate' ]]; then - for lang in "${LANGUAGE[@]}"; do - yarn workspace tests format $lang - done - yarn cts:lint:scripts --fix -fi diff --git a/scripts/oraLog.ts b/scripts/oraLog.ts new file mode 100644 index 0000000000..20ece0f229 --- /dev/null +++ b/scripts/oraLog.ts @@ -0,0 +1,77 @@ +import ora from 'ora-classic'; + +import { CI } from './common'; + +type OraLogOptions = { text?: string; indent?: number }; +class OraLog { + private _text: string; + private _indent = 0; + + constructor(options: OraLogOptions | string) { + if (typeof options === 'string') { + this._text = options; + } else { + this._text = options.text ?? ''; + this._indent = options.indent ?? 0; + } + } + + private maybeText(text?: string): void { + if (text !== undefined) { + this._text = text; + this.start(); + } + } + + start(): this { + // eslint-disable-next-line no-console + console.log(' '.repeat(this._indent) + this._text); + return this; + } + + succeed(text?: string): void { + this.maybeText(text); + } + + fail(text?: string): void { + this.maybeText(text); + } + + warn(text?: string): void { + this.maybeText(text); + } + + info(text?: string): void { + this.maybeText(text); + } + + get text(): string { + return this._text; + } + + set text(text: string) { + this._text = text; + this.start(); + } + + get indent(): number { + return this._indent; + } + + set indent(indent: number) { + this._indent = indent; + } +} + +/** + * Returns a spinner that will log directly in verbose mode, to avoid conflict with other log. + */ +export function createSpinner( + options: OraLogOptions | string, + verbose: boolean +): ora.Ora | OraLog { + if (verbose || CI) { + return new OraLog(options); + } + return ora(options); +} diff --git a/scripts/package.json b/scripts/package.json index de616795ba..e48daed9bb 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -5,20 +5,32 @@ "build": "tsc", "createReleaseIssue": "yarn build && node dist/scripts/release/create-release-issue.js", "processRelease": "yarn build && node dist/scripts/release/process-release.js", - "setHostsOptions": "yarn build && node dist/scripts/pre-gen/setHostsOptions.js", "test": "jest" }, "devDependencies": { + "@apidevtools/swagger-parser": "10.0.3", "@octokit/rest": "18.12.0", + "@types/folder-hash": "4.0.1", + "@types/inquirer": "8.2.0", + "@types/jest": "27.4.0", "@types/js-yaml": "4.0.5", + "@types/mustache": "4.1.2", "@types/node": "16.11.11", "@types/semver": "7.3.9", + "commander": "9.0.0", "dotenv": "16.0.0", + "eslint": "8.6.0", "execa": "5.1.1", + "folder-hash": "4.0.2", + "inquirer": "8.2.0", "jest": "27.4.7", "js-yaml": "4.1.0", + "mustache": "4.2.0", + "openapi-types": "10.0.0", + "ora-classic": "5.4.2", "semver": "7.3.5", "ts-jest": "27.1.3", + "ts-node": "10.5.0", "typescript": "4.5.4" } } diff --git a/scripts/playground.sh b/scripts/playground.sh deleted file mode 100755 index e422a0ee1b..0000000000 --- a/scripts/playground.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -# Break on non-zero code -set -e - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -# Move to the root (easier to locate other scripts) -cd ${DIR}/.. - -LANGUAGE=$1 -CLIENT=$2 - -# Run the pre generation script if it exists. -run_playground() { - if [[ $LANGUAGE == 'javascript' ]]; then - yarn workspace javascript-playground start:$CLIENT - elif [[ $LANGUAGE == 'java' ]]; then - ./gradle/gradlew --no-daemon -p playground/java run - elif [[ $lang == 'php' ]]; then - cd playground/php - composer update - composer dump-autoload - cd src - php8 $client.php - fi -} - -run_playground diff --git a/scripts/playground.ts b/scripts/playground.ts new file mode 100644 index 0000000000..da7fe28a6f --- /dev/null +++ b/scripts/playground.ts @@ -0,0 +1,32 @@ +import { run } from './common'; +import type { Generator } from './types'; + +export async function playground({ + language, + client, +}: Generator): Promise { + const verbose = true; + switch (language) { + case 'javascript': + await run(`yarn workspace javascript-playground start:${client}`, { + verbose, + }); + break; + case 'java': + await run(`./gradle/gradlew --no-daemon -p playground/java run`, { + verbose, + }); + break; + case 'php': + await run( + `cd playground/php && \ + composer update && \ + composer dump-autoload && \ + cd src && \ + php8 ${client}.php`, + { verbose } + ); + break; + default: + } +} diff --git a/scripts/post-gen/global.sh b/scripts/post-gen/global.sh deleted file mode 100755 index b8d96b7868..0000000000 --- a/scripts/post-gen/global.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -# Break on non-zero code -set -e - -LANGUAGE=$1 - -if [[ $CI ]]; then - echo "Not running post-gen/global on CI for $LANGUAGE" - - exit 0 -fi - -if [[ ! $DOCKER ]]; then - echo "You should run scripts via the docker container, see README.md" - - exit 1 -fi - -build_js_utils() { - echo "> Cleaning JavaScript client utils..." - yarn workspace algoliasearch-client-javascript clean:utils - - echo "> Building JavaScript client utils..." - yarn workspace algoliasearch-client-javascript build:utils - - echo "" -} - -format_specs() { - echo "> Formatting specs..." - - CMD="yarn specs:fix" - if [[ $VERBOSE == "true" ]]; then - $CMD - else - set +e - log=$($CMD) - - if [[ $? != 0 ]]; then - echo "$log" - exit 1 - fi - set -e - fi - - echo "" -} - -format_specs - -if [[ $LANGUAGE == 'javascript' || $LANGUAGE == 'all' ]]; then - build_js_utils -fi diff --git a/scripts/post-gen/java.sh b/scripts/post-gen/java.sh index fcf26b8fcf..17f1548a84 100755 --- a/scripts/post-gen/java.sh +++ b/scripts/post-gen/java.sh @@ -3,6 +3,10 @@ # Break on non-zero code set -e +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +# Move to the root (easier to locate other scripts) +cd ${DIR}/../.. + FOLDER=$1 # Restore the oneOf spec diff --git a/scripts/post-gen/javascript.sh b/scripts/post-gen/javascript.sh index 6d0c89335c..1f85a47272 100755 --- a/scripts/post-gen/javascript.sh +++ b/scripts/post-gen/javascript.sh @@ -3,6 +3,11 @@ # Break on non-zero code set -e +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +# Move to the root (easier to locate other scripts) +cd ${DIR}/../.. + + FOLDER=$1 GENERATOR=$2 diff --git a/scripts/post-gen/php.sh b/scripts/post-gen/php.sh index df070ad7e1..7ebcb5aa15 100755 --- a/scripts/post-gen/php.sh +++ b/scripts/post-gen/php.sh @@ -1,5 +1,10 @@ #!/bin/bash set -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +# Move to the root (easier to locate other scripts) +cd ${DIR}/../.. + FOLDER=$1 #Move Configuration file diff --git a/scripts/pre-gen/java.sh b/scripts/pre-gen/java.sh index aed3543106..2c5fb11e59 100755 --- a/scripts/pre-gen/java.sh +++ b/scripts/pre-gen/java.sh @@ -3,6 +3,10 @@ # Break on non-zero code set -e +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +# Move to the root (easier to locate other scripts) +cd ${DIR}/../.. + # Remove the oneOf in the spec until it's supported (https://github.com/OpenAPITools/openapi-generator/issues/10880). # Remove the oneOf and only keep the last $ref diff --git a/scripts/pre-gen/setHostsOptions.ts b/scripts/pre-gen/setHostsOptions.ts index 872a5837bf..e60fea2063 100644 --- a/scripts/pre-gen/setHostsOptions.ts +++ b/scripts/pre-gen/setHostsOptions.ts @@ -1,9 +1,11 @@ import { readFile, stat, writeFile } from 'fs/promises'; -import path from 'path'; import { URL } from 'url'; import yaml from 'js-yaml'; +import { toAbsolutePath } from '../common'; +import type { Generator } from '../types'; + type Server = { url: string; variables?: { @@ -31,8 +33,11 @@ type AdditionalProperties = Partial<{ experimentalHost: string; }>; -async function setHostsOptions(): Promise { - const openapitoolsPath = path.join(process.cwd(), '../openapitools.json'); +export async function setHostsOptions({ + client, + key: generator, +}: Pick): Promise { + const openapitoolsPath = toAbsolutePath('openapitools.json'); if (!(await stat(openapitoolsPath))) { throw new Error( `File not found ${openapitoolsPath}.\nMake sure your run scripts from the root directory using yarn workspace.` @@ -40,15 +45,13 @@ async function setHostsOptions(): Promise { } const openapitools = JSON.parse(await readFile(openapitoolsPath, 'utf-8')); - const [language, client] = process.argv.slice(2); - const generator = `${language}-${client}`; const generatorOptions = openapitools['generator-cli'].generators[generator]; if (!generator || !generatorOptions) { throw new Error(`Generator not found: ${generator}`); } - const specPath = path.join(process.cwd(), `../specs/bundled/${client}.yml`); + const specPath = toAbsolutePath(`specs/bundled/${client}.yml`); if (!(await stat(specPath))) { throw new Error( @@ -115,5 +118,3 @@ async function setHostsOptions(): Promise { throw new Error(`Error reading yaml file ${generator}: ${e}`); } } - -setHostsOptions(); diff --git a/scripts/release/common.ts b/scripts/release/common.ts index 49dc49e23b..61d154dcf0 100644 --- a/scripts/release/common.ts +++ b/scripts/release/common.ts @@ -1,6 +1,3 @@ -import execa from 'execa'; // https://github.com/sindresorhus/execa/tree/v5.1.1 - -import openapitools from '../../openapitools.json'; import config from '../../release.config.json'; export const RELEASED_TAG = config.releasedTag; @@ -8,40 +5,6 @@ export const MAIN_BRANCH = config.mainBranch; export const OWNER = config.owner; export const REPO = config.repo; -export type Run = ( - command: string, - options?: Partial< - execa.SyncOptions & { - errorMessage: string; - } - > -) => execa.ExecaReturnBase['stdout']; - -export const run: Run = ( - command, - { errorMessage = undefined, ...execaOptions } = {} -) => { - let result: execa.ExecaSyncReturnValue; - try { - result = execa.commandSync(command, execaOptions); - } catch (err) { - if (errorMessage) { - throw new Error(`[ERROR] ${errorMessage}`); - } else { - throw err; - } - } - return result.stdout; -}; - -export const LANGS = [ - ...new Set( - Object.keys(openapitools['generator-cli'].generators).map( - (key) => key.split('-')[0] - ) - ), -]; - export function getMarkdownSection(markdown: string, title: string): string { const levelIndicator = title.split(' ')[0]; // e.g. `##` const lines = markdown diff --git a/scripts/release/create-release-issue.ts b/scripts/release/create-release-issue.ts index 9d99fbea4c..d425a2597f 100755 --- a/scripts/release/create-release-issue.ts +++ b/scripts/release/create-release-issue.ts @@ -3,9 +3,9 @@ import { Octokit } from '@octokit/rest'; import dotenv from 'dotenv'; import semver from 'semver'; -import openapitools from '../../openapitools.json'; +import { GENERATORS, LANGUAGES, run } from '../common'; -import { RELEASED_TAG, MAIN_BRANCH, OWNER, REPO, LANGS, run } from './common'; +import { RELEASED_TAG, MAIN_BRANCH, OWNER, REPO } from './common'; import TEXT from './text'; dotenv.config(); @@ -25,14 +25,11 @@ type Versions = { function readVersions(): Versions { const versions = {}; - const generators = openapitools['generator-cli'].generators; - - Object.keys(generators).forEach((generator) => { - const lang = generator.split('-')[0]; - if (!versions[lang]) { - versions[lang] = { - current: generators[generator].additionalProperties.packageVersion, - langName: lang, + Object.values(GENERATORS).forEach((gen) => { + if (!versions[gen.language]) { + versions[gen.language] = { + current: gen.additionalProperties.packageVersion, + langName: gen.language, next: undefined, }; } @@ -44,182 +41,188 @@ if (!process.env.GITHUB_TOKEN) { throw new Error('Environment variable `GITHUB_TOKEN` does not exist.'); } -if (run('git rev-parse --abbrev-ref HEAD') !== MAIN_BRANCH) { - throw new Error( - `You can run this script only from \`${MAIN_BRANCH}\` branch.` - ); -} +async function createReleaseIssue(): Promise { + if ((await run('git rev-parse --abbrev-ref HEAD')) !== MAIN_BRANCH) { + throw new Error( + `You can run this script only from \`${MAIN_BRANCH}\` branch.` + ); + } -if (run('git status --porcelain')) { - throw new Error( - 'Working directory is not clean. Commit all the changes first.' - ); -} + if (await run('git status --porcelain')) { + throw new Error( + 'Working directory is not clean. Commit all the changes first.' + ); + } -run(`git rev-parse --verify refs/tags/${RELEASED_TAG}`, { - errorMessage: '`released` tag is missing in this repository.', -}); + await run(`git rev-parse --verify refs/tags/${RELEASED_TAG}`, { + errorMessage: '`released` tag is missing in this repository.', + }); -// Reading versions from `openapitools.json` -const versions = readVersions(); + // Reading versions from `openapitools.json` + const versions = readVersions(); + + console.log('Pulling from origin...'); + run(`git pull origin ${MAIN_BRANCH}`); + + console.log('Pushing to origin...'); + run(`git push origin ${MAIN_BRANCH}`); + + const commitsWithoutScope: string[] = []; + const commitsWithNonLanguageScope: string[] = []; + + // Reading commits since last release + type LatestCommit = { + hash: string; + type: string; + lang: string; + message: string; + raw: string; + }; + const latestCommits = ( + await run(`git log --oneline ${RELEASED_TAG}..${MAIN_BRANCH}`) + ) + .split('\n') + .filter(Boolean) + .map((commit) => { + const hash = commit.slice(0, 7); + let message = commit.slice(8); + let type = message.slice(0, message.indexOf(':')); + const matchResult = type.match(/(.+)\((.+)\)/); + if (!matchResult) { + commitsWithoutScope.push(commit); + return undefined; + } + message = message.slice(message.indexOf(':') + 1).trim(); + type = matchResult[1]; + const lang = matchResult[2]; + + if (!LANGUAGES.includes(lang)) { + commitsWithNonLanguageScope.push(commit); + return undefined; + } + + return { + hash, + type, // `fix` | `feat` | `chore` | ... + lang, // `javascript` | `php` | `java` | ... + message, + raw: commit, + }; + }) + .filter(Boolean) as LatestCommit[]; -console.log('Pulling from origin...'); -run(`git pull origin ${MAIN_BRANCH}`); + console.log('[INFO] Skipping these commits due to lack of language scope:'); + console.log(commitsWithoutScope.map((commit) => ` ${commit}`).join('\n')); -console.log('Pushing to origin...'); -run(`git push origin ${MAIN_BRANCH}`); + console.log(''); + console.log('[INFO] Skipping these commits due to wrong scopes:'); + console.log( + commitsWithNonLanguageScope.map((commit) => ` ${commit}`).join('\n') + ); -const commitsWithoutScope: string[] = []; -const commitsWithNonLanguageScope: string[] = []; + LANGUAGES.forEach((lang) => { + const commits = latestCommits.filter( + (lastestCommit) => lastestCommit.lang === lang + ); + const currentVersion = versions[lang].current; -// Reading commits since last release -type LatestCommit = { - hash: string; - type: string; - lang: string; - message: string; - raw: string; -}; -const latestCommits = run(`git log --oneline ${RELEASED_TAG}..${MAIN_BRANCH}`) - .split('\n') - .filter(Boolean) - .map((commit) => { - const hash = commit.slice(0, 7); - let message = commit.slice(8); - let type = message.slice(0, message.indexOf(':')); - const matchResult = type.match(/(.+)\((.+)\)/); - if (!matchResult) { - commitsWithoutScope.push(commit); - return undefined; + if (commits.length === 0) { + versions[lang].next = currentVersion; + versions[lang].noCommit = true; + return; } - message = message.slice(message.indexOf(':') + 1).trim(); - type = matchResult[1]; - const lang = matchResult[2]; - if (!LANGS.includes(lang)) { - commitsWithNonLanguageScope.push(commit); - return undefined; + if (semver.prerelease(currentVersion)) { + // if version is like 0.1.2-beta.1, it increases to 0.1.2-beta.2, even if there's a breaking change. + versions[lang].next = semver.inc(currentVersion, 'prerelease'); + return; } - return { - hash, - type, // `fix` | `feat` | `chore` | ... - lang, // `javascript` | `php` | `java` | ... - message, - raw: commit, - }; - }) - .filter(Boolean) as LatestCommit[]; - -console.log('[INFO] Skipping these commits due to lack of language scope:'); -console.log(commitsWithoutScope.map((commit) => ` ${commit}`).join('\n')); - -console.log(''); -console.log('[INFO] Skipping these commits due to wrong scopes:'); -console.log( - commitsWithNonLanguageScope.map((commit) => ` ${commit}`).join('\n') -); - -LANGS.forEach((lang) => { - const commits = latestCommits.filter( - (lastestCommit) => lastestCommit.lang === lang - ); - const currentVersion = versions[lang].current; - - if (commits.length === 0) { - versions[lang].next = currentVersion; - versions[lang].noCommit = true; - return; - } - - if (semver.prerelease(currentVersion)) { - // if version is like 0.1.2-beta.1, it increases to 0.1.2-beta.2, even if there's a breaking change. - versions[lang].next = semver.inc(currentVersion, 'prerelease'); - return; - } - - if (commits.some((commit) => commit.message.includes('BREAKING CHANGE'))) { - versions[lang].next = semver.inc(currentVersion, 'major'); - return; - } - - const commitTypes = new Set(commits.map(({ type }) => type)); - if (commitTypes.has('feat')) { - versions[lang].next = semver.inc(currentVersion, 'minor'); - return; - } + if (commits.some((commit) => commit.message.includes('BREAKING CHANGE'))) { + versions[lang].next = semver.inc(currentVersion, 'major'); + return; + } - versions[lang].next = semver.inc(currentVersion, 'patch'); - if (!commitTypes.has('fix')) { - versions[lang].skipRelease = true; - } -}); + const commitTypes = new Set(commits.map(({ type }) => type)); + if (commitTypes.has('feat')) { + versions[lang].next = semver.inc(currentVersion, 'minor'); + return; + } -const versionChanges = LANGS.map((lang) => { - const { current, next, noCommit, skipRelease, langName } = versions[lang]; + versions[lang].next = semver.inc(currentVersion, 'patch'); + if (!commitTypes.has('fix')) { + versions[lang].skipRelease = true; + } + }); - if (noCommit) { - return `- ~${langName}: v${current} (${TEXT.noCommit})~`; - } + const versionChanges = LANGUAGES.map((lang) => { + const { current, next, noCommit, skipRelease, langName } = versions[lang]; - if (!current) { - return `- ~${langName}: (${TEXT.currentVersionNotFound})~`; - } + if (noCommit) { + return `- ~${langName}: v${current} (${TEXT.noCommit})~`; + } - const checked = skipRelease ? ' ' : 'x'; - return [ - `- [${checked}] ${langName}: v${current} -> v${next}`, - skipRelease && TEXT.descriptionForSkippedLang(langName), - ] - .filter(Boolean) - .join('\n'); -}).join('\n'); - -const changelogs = LANGS.filter( - (lang) => !versions[lang].noCommit && versions[lang].current -) - .flatMap((lang) => { - if (versions[lang].noCommit) { - return []; + if (!current) { + return `- ~${langName}: (${TEXT.currentVersionNotFound})~`; } + const checked = skipRelease ? ' ' : 'x'; return [ - `### ${versions[lang].langName}`, - ...latestCommits - .filter((commit) => commit.lang === lang) - .map((commit) => `- ${commit.raw}`), - ]; - }) - .join('\n'); - -const body = [ - TEXT.header, - TEXT.versionChangeHeader, - versionChanges, - TEXT.changelogHeader, - TEXT.changelogDescription, - changelogs, - TEXT.approvalHeader, - TEXT.approval, -].join('\n\n'); - -const octokit = new Octokit({ - auth: `token ${process.env.GITHUB_TOKEN}`, -}); - -octokit.rest.issues - .create({ - owner: OWNER, - repo: REPO, - title: `chore: release ${new Date().toISOString().split('T')[0]}`, - body, - }) - .then((result) => { - const { - data: { number, html_url: url }, - } = result; - - console.log(''); - console.log(`Release issue #${number} is ready for review.`); - console.log(` > ${url}`); + `- [${checked}] ${langName}: v${current} -> v${next}`, + skipRelease && TEXT.descriptionForSkippedLang(langName), + ] + .filter(Boolean) + .join('\n'); + }).join('\n'); + + const changelogs = LANGUAGES.filter( + (lang) => !versions[lang].noCommit && versions[lang].current + ) + .flatMap((lang) => { + if (versions[lang].noCommit) { + return []; + } + + return [ + `### ${versions[lang].langName}`, + ...latestCommits + .filter((commit) => commit.lang === lang) + .map((commit) => `- ${commit.raw}`), + ]; + }) + .join('\n'); + + const body = [ + TEXT.header, + TEXT.versionChangeHeader, + versionChanges, + TEXT.changelogHeader, + TEXT.changelogDescription, + changelogs, + TEXT.approvalHeader, + TEXT.approval, + ].join('\n\n'); + + const octokit = new Octokit({ + auth: `token ${process.env.GITHUB_TOKEN}`, }); + + octokit.rest.issues + .create({ + owner: OWNER, + repo: REPO, + title: `chore: release ${new Date().toISOString().split('T')[0]}`, + body, + }) + .then((result) => { + const { + data: { number, html_url: url }, + } = result; + + console.log(''); + console.log(`Release issue #${number} is ready for review.`); + console.log(` > ${url}`); + }); +} + +createReleaseIssue(); diff --git a/scripts/release/process-release.ts b/scripts/release/process-release.ts index d8bb376be6..e95fa565fa 100755 --- a/scripts/release/process-release.ts +++ b/scripts/release/process-release.ts @@ -1,30 +1,17 @@ /* eslint-disable no-console */ import fs from 'fs'; -import path from 'path'; import dotenv from 'dotenv'; import execa from 'execa'; import openapitools from '../../openapitools.json'; +import { toAbsolutePath, run } from '../common'; -import type { Run } from './common'; -import { - MAIN_BRANCH, - OWNER, - REPO, - run as runOriginal, - getMarkdownSection, -} from './common'; +import { MAIN_BRANCH, OWNER, REPO, getMarkdownSection } from './common'; import TEXT from './text'; -// This script is run by `yarn workspace ...`, which means the current working directory is `./script` -const ROOT_DIR = path.resolve(process.cwd(), '..'); - dotenv.config(); -const run: Run = (command, options = {}) => - runOriginal(command, { cwd: ROOT_DIR, ...options }); - if (!process.env.GITHUB_TOKEN) { throw new Error('Environment variable `GITHUB_TOKEN` does not exist.'); } @@ -33,121 +20,122 @@ if (!process.env.EVENT_NUMBER) { throw new Error('Environment variable `EVENT_NUMBER` does not exist.'); } -const issueBody = JSON.parse( - execa.sync('curl', [ - '-H', - `Authorization: token ${process.env.GITHUB_TOKEN}`, - `https://api.github.com/repos/${OWNER}/${REPO}/issues/${process.env.EVENT_NUMBER}`, - ]).stdout -).body; +async function processRelease(): Promise { + const issueBody = JSON.parse( + execa.sync('curl', [ + '-H', + `Authorization: token ${process.env.GITHUB_TOKEN}`, + `https://api.github.com/repos/${OWNER}/${REPO}/issues/${process.env.EVENT_NUMBER}`, + ]).stdout + ).body; + + if ( + !getMarkdownSection(issueBody, TEXT.approvalHeader) + .split('\n') + .find((line) => line.startsWith(`- [x] ${TEXT.approved}`)) + ) { + throw new Error('The issue was not approved.'); + } -if ( - !getMarkdownSection(issueBody, TEXT.approvalHeader) + const versionsToRelease = {}; + getMarkdownSection(issueBody, TEXT.versionChangeHeader) .split('\n') - .find((line) => line.startsWith(`- [x] ${TEXT.approved}`)) -) { - throw new Error('The issue was not approved.'); -} - -const versionsToRelease = {}; -getMarkdownSection(issueBody, TEXT.versionChangeHeader) - .split('\n') - .forEach((line) => { - const result = line.match(/- \[x\] (.+): v(.+) -> v(.+)/); - if (!result) { - return; + .forEach((line) => { + const result = line.match(/- \[x\] (.+): v(.+) -> v(.+)/); + if (!result) { + return; + } + const [, lang, current, next] = result; + versionsToRelease[lang] = { + current, + next, + }; + }); + + const langsToUpdateRepo = getMarkdownSection( + issueBody, + TEXT.versionChangeHeader + ) + .split('\n') + .map((line) => { + const result = line.match(/- \[ \] (.+): v(.+) -> v(.+)/); + return result?.[1]; + }) + .filter(Boolean); // e.g. ['javascript', 'php'] + + // update versions in `openapitools.json` + Object.keys(openapitools['generator-cli'].generators).forEach((client) => { + const lang = client.split('-')[0]; + if (versionsToRelease[lang]) { + openapitools['generator-cli'].generators[ + client + ].additionalProperties.packageVersion = versionsToRelease[lang].next; } - const [, lang, current, next] = result; - versionsToRelease[lang] = { - current, - next, - }; }); + fs.writeFileSync( + toAbsolutePath('openapitools.json'), + JSON.stringify(openapitools, null, 2) + ); + + // update changelogs + new Set([...Object.keys(versionsToRelease), ...langsToUpdateRepo]).forEach( + (lang) => { + const filePath = toAbsolutePath(`docs/changelogs/${lang}.md`); + const header = versionsToRelease[lang!] + ? `## ${versionsToRelease[lang!].next}` + : `## ${new Date().toISOString().split('T')[0]}`; + const newChangelog = getMarkdownSection( + getMarkdownSection(issueBody, TEXT.changelogHeader), + `### ${lang}` + ); + const existingContent = fs.readFileSync(filePath).toString(); + fs.writeFileSync( + filePath, + [header, newChangelog, existingContent].join('\n\n') + ); + } + ); + + // commit openapitools and changelogs + if (process.env.RELEASE_TEST !== 'true') { + await run('git config user.name "api-clients-bot"'); + await run('git config user.email "bot@algolia.com"'); + await run('git add openapitools.json'); + await run('git add doc/changelogs/*'); + execa.sync('git', ['commit', '-m', TEXT.commitMessage]); + await run(`git push origin ${MAIN_BRANCH}`); + } -const langsToUpdateRepo = getMarkdownSection( - issueBody, - TEXT.versionChangeHeader -) - .split('\n') - .map((line) => { - const result = line.match(/- \[ \] (.+): v(.+) -> v(.+)/); - return result?.[1]; - }) - .filter(Boolean); // e.g. ['javascript', 'php'] - -// update versions in `openapitools.json` -Object.keys(openapitools['generator-cli'].generators).forEach((client) => { - const lang = client.split('-')[0]; - if (versionsToRelease[lang]) { - openapitools['generator-cli'].generators[ - client - ].additionalProperties.packageVersion = versionsToRelease[lang].next; + // generate clients to release + for (const lang of Object.keys(versionsToRelease)) { + console.log(`Generating ${lang} client(s)...`); + await run(`yarn cli generate ${lang}`); } -}); -fs.writeFileSync( - path.resolve(ROOT_DIR, 'openapitools.json'), - JSON.stringify(openapitools, null, 2) -); - -// update changelogs -new Set([...Object.keys(versionsToRelease), ...langsToUpdateRepo]).forEach( - (lang) => { - const filePath = path.resolve(ROOT_DIR, `docs/changelogs/${lang}.md`); - const header = versionsToRelease[lang!] - ? `## ${versionsToRelease[lang!].next}` - : `## ${new Date().toISOString().split('T')[0]}`; - const newChangelog = getMarkdownSection( - getMarkdownSection(issueBody, TEXT.changelogHeader), - `### ${lang}` - ); - const existingContent = fs.readFileSync(filePath).toString(); - fs.writeFileSync( - filePath, - [header, newChangelog, existingContent].join('\n\n') - ); + + // generate clients to just update the repos + for (const lang of langsToUpdateRepo) { + console.log(`Generating ${lang} client(s)...`); + await run(`yarn cli generate ${lang}`, { verbose: true }); } -); - -// commit openapitools and changelogs -if (process.env.RELEASE_TEST !== 'true') { - run('git config user.name "api-clients-bot"'); - run('git config user.email "bot@algolia.com"'); - run('git add openapitools.json'); - run('git add doc/changelogs/*'); - execa.sync('git', ['commit', '-m', TEXT.commitMessage]); - run(`git push origin ${MAIN_BRANCH}`); -} -// generate clients to release -Object.keys(versionsToRelease).forEach((lang) => { - console.log(`Generating ${lang} client(s)...`); - // @ts-expect-error the library `execa` is not typed correctly - run(`yarn generate ${lang}`).pipe(process.stdout); -}); - -// generate clients to just update the repos -langsToUpdateRepo.forEach((lang) => { - console.log(`Generating ${lang} client(s)...`); - // @ts-expect-error the library `execa` is not typed correctly - run(`yarn generate ${lang}`).pipe(process.stdout); -}); - -const clientPath = path.resolve( - ROOT_DIR, - 'clients/dummy-algoliasearch-client-javascript' -); -const runInClient: Run = (command, options = {}) => - runOriginal(command, { + const clientPath = toAbsolutePath( + 'clients/dummy-algoliasearch-client-javascript' + ); + const runInClient = (command: string, options = {}): Promise => + run(command, { + cwd: clientPath, + ...options, + }); + + await runInClient(`git checkout next`); + await run( + `cp -r clients/algoliasearch-client-javascript/ clients/dummy-algoliasearch-client-javascript` + ); + await runInClient(`git add .`); + execa.sync('git', ['commit', '-m', 'chore: release test'], { cwd: clientPath, - ...options, }); + await runInClient(`git push origin next`); +} -runInClient(`git checkout next`); -run( - `cp -r clients/algoliasearch-client-javascript/ clients/dummy-algoliasearch-client-javascript` -); -runInClient(`git add .`); -execa.sync('git', ['commit', '-m', 'chore: release test'], { - cwd: clientPath, -}); -runInClient(`git push origin next`); +processRelease(); diff --git a/scripts/runCTS.sh b/scripts/runCTS.sh deleted file mode 100755 index 28fa3ab056..0000000000 --- a/scripts/runCTS.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# Break on non-zero code -set -e - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -# Move to the root (easier to locate other scripts) -cd ${DIR}/.. - -LANGUAGE=$1 - -# Run the pre generation script if it exists. -run_cts() { - if [[ $LANGUAGE == 'javascript' ]]; then - yarn workspace javascript-tests test - elif [[ $LANGUAGE == 'php' ]]; then - if [[ $CI ]]; then - PHP="php" - else - PHP="php8" - fi - $PHP ./clients/algoliasearch-client-php/vendor/bin/phpunit ./ - elif [[ $LANGUAGE == 'java' ]]; then - ./gradle/gradlew --no-daemon -p tests/output/java test - else - echo "Skipping unknown language $LANGUAGE to run the CTS" - exit 0 - fi -} - -run_cts diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index 34cc31a9fd..ab90a217cc 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -5,6 +5,6 @@ "types": ["node", "jest"], "outDir": "dist" }, - "include": ["pre-gen/setHostsOptions.ts", "release/*"], - "exclude": ["dist", "*.json"] + "include": ["**/*.ts"], + "exclude": ["dist", "*.json", "node_modules"] } diff --git a/scripts/types.ts b/scripts/types.ts new file mode 100644 index 0000000000..c2dcba8f55 --- /dev/null +++ b/scripts/types.ts @@ -0,0 +1,11 @@ +export type Generator = Record & { + language: string; + client: string; + key: string; +}; + +export type RunOptions = { + errorMessage?: string; + verbose?: boolean; + cwd?: string; +}; diff --git a/tests/CTS/config.json b/tests/CTS/config.json deleted file mode 100644 index 0f8a91e9b1..0000000000 --- a/tests/CTS/config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "javascript": { - "extension": "test.ts", - "outputFolder": "src" - }, - "java": { - "extension": "test.java", - "outputFolder": "src/test/java/com/algolia" - } -} diff --git a/tests/jest.config.ts b/tests/jest.config.ts deleted file mode 100644 index d2cc09a672..0000000000 --- a/tests/jest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Config } from '@jest/types'; - -const config: Config.InitialOptions = { - preset: 'ts-jest', - testEnvironment: 'node', -}; - -export default config; diff --git a/tests/output/javascript/tsconfig.json b/tests/output/javascript/tsconfig.json index 27113bfb38..c5cb759b73 100644 --- a/tests/output/javascript/tsconfig.json +++ b/tests/output/javascript/tsconfig.json @@ -1,9 +1,10 @@ { - "extends": "../../tsconfig.json", + "extends": "../../../base.tsconfig.json", "compilerOptions": { "typeRoots": ["../../../node_modules/@types"], + "types": ["node", "jest"], "outDir": "dist" }, "include": ["src"], - "exclude": [] + "exclude": ["dist", "node_modules"] } diff --git a/tests/package.json b/tests/package.json deleted file mode 100644 index 5acd34f04a..0000000000 --- a/tests/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "tests", - "version": "1.0.0", - "workspaces": [ - "output/javascript" - ], - "scripts": { - "build": "tsc", - "generate": "yarn generate:methods:requets ${0:-javascript} ${1:-search} && yarn generate:client ${0:-javascript} ${1:-search}", - "generate:methods:requets": "node dist/tests/src/methods/requests/main.js ${0:-javascript} ${1:-search}", - "generate:client": "node dist/tests/src/client/main.js ${0:-javascript} ${1:-search}", - "format": "../scripts/formatter.sh ${0:-javascript} tests/output/${0:-javascript}/src && yarn lint", - "lint": "eslint --ext=ts ./src", - "test:scripts": "jest" - }, - "devDependencies": { - "@apidevtools/swagger-parser": "10.0.3", - "@types/jest": "27.4.0", - "@types/mustache": "4.1.2", - "@types/node": "16.11.11", - "eslint": "8.6.0", - "jest": "27.4.7", - "mustache": "4.2.0", - "openapi-types": "10.0.0", - "ts-jest": "27.1.3", - "typescript": "4.5.4" - } -} diff --git a/tests/src/client/main.ts b/tests/src/client/main.ts deleted file mode 100644 index 0d4476ffa3..0000000000 --- a/tests/src/client/main.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { parseCLI, checkIfLanguageExists } from '../utils'; - -import { generateTests } from './generate'; - -async function main(): Promise { - const { lang, client } = parseCLI(process.argv, 'generate:client'); - - if (!checkIfLanguageExists(lang)) { - // eslint-disable-next-line no-console - console.log( - `Skipping CTS generation > generate:client for ${lang}-${client}: Language not present in the config.json file` - ); - - return; - } - - // eslint-disable-next-line no-console - console.log(`Generating CTS > generate:client for ${lang}-${client}`); - - try { - await generateTests(lang, client); - } catch (e) { - if (e instanceof Error) { - // eslint-disable-next-line no-console - console.error(e); - } - } -} - -main(); diff --git a/tests/src/methods/requests/generate.ts b/tests/src/methods/requests/generate.ts deleted file mode 100644 index 4d14c9f4f7..0000000000 --- a/tests/src/methods/requests/generate.ts +++ /dev/null @@ -1,60 +0,0 @@ -import fsp from 'fs/promises'; - -import Mustache from 'mustache'; - -import openapitools from '../../../../openapitools.json'; -import { - createClientName, - packageNames, - capitalize, - getOutputPath, - createOutputDir, - loadTemplates, -} from '../../utils'; - -import { loadCTS } from './cts'; - -const testPath = 'methods/requests'; - -export async function generateTests( - language: string, - client: string -): Promise { - const { requests: template, ...partialTemplates } = await loadTemplates({ - language, - testPath, - }); - const cts = (await loadCTS(client)).requests; - await createOutputDir({ language, testPath }); - - if (cts.length === 0) { - return; - } - - const code = Mustache.render( - template, - { - import: packageNames[language][client], - client: createClientName(client, language), - blocks: cts, - hasRegionalHost: openapitools['generator-cli'].generators[ - `${language}-${client}` - ].additionalProperties.hasRegionalHost - ? true - : undefined, - capitalize() { - return function (text: string, render: (string) => string): string { - return capitalize(render(text)); - }; - }, - escapeQuotes() { - return function (text: string, render: (string) => string): string { - return render(text).replace(/"/g, '\\"'); - }; - }, - }, - partialTemplates - ); - - await fsp.writeFile(getOutputPath({ language, client, testPath }), code); -} diff --git a/tests/src/methods/requests/main.ts b/tests/src/methods/requests/main.ts deleted file mode 100644 index 9a5fd15ecd..0000000000 --- a/tests/src/methods/requests/main.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { parseCLI, checkIfLanguageExists } from '../../utils'; - -import { generateTests } from './generate'; - -async function main(): Promise { - const { lang, client } = parseCLI(process.argv, 'generate:methods:requests'); - - if (!checkIfLanguageExists(lang)) { - // eslint-disable-next-line no-console - console.log( - `Skipping CTS generation > generate:methods:requests for ${lang}-${client}: Language not present in the config.json file` - ); - - return; - } - - // eslint-disable-next-line no-console - console.log( - `Generating CTS > generate:methods:requests for ${lang}-${client}` - ); - - try { - await generateTests(lang, client); - } catch (e) { - if (e instanceof Error) { - // eslint-disable-next-line no-console - console.error(e); - } - } -} - -main(); diff --git a/tests/tsconfig.json b/tests/tsconfig.json deleted file mode 100644 index a9efb8ab01..0000000000 --- a/tests/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../base.tsconfig.json", - "compilerOptions": { - "typeRoots": ["../node_modules/@types"], - "types": ["node", "jest"], - "lib": ["ESNext", "dom", "dom.iterable"], - "outDir": "dist" - }, - "include": ["src"], - "exclude": ["dist", "output"] -} diff --git a/yarn.lock b/yarn.lock index 4dfa561300..e7e2b440aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3073,6 +3073,13 @@ __metadata: languageName: node linkType: hard +"@types/folder-hash@npm:4.0.1": + version: 4.0.1 + resolution: "@types/folder-hash@npm:4.0.1" + checksum: df14059d4ba6bde347e078f3d00a2a93965d5e5eb9c9f9c3457b1505fb6bb6fca08083bab20cd7221d2b19c0bcf5a60dc7819720bd8f3ed226b75c3a08dae227 + languageName: node + linkType: hard + "@types/glob@npm:*": version: 7.2.0 resolution: "@types/glob@npm:7.2.0" @@ -3092,6 +3099,16 @@ __metadata: languageName: node linkType: hard +"@types/inquirer@npm:8.2.0": + version: 8.2.0 + resolution: "@types/inquirer@npm:8.2.0" + dependencies: + "@types/through": "*" + rxjs: ^7.2.0 + checksum: bb4b550ca01e892bab9483dfc8f8122dc4eb3291e8b9e1d038fbb6d883335f6ff61692331ae90af452e878c0335b119269ad9909c1ba4b6ec10da6fcf1568011 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.4 resolution: "@types/istanbul-lib-coverage@npm:2.0.4" @@ -3240,6 +3257,15 @@ __metadata: languageName: node linkType: hard +"@types/through@npm:*": + version: 0.0.30 + resolution: "@types/through@npm:0.0.30" + dependencies: + "@types/node": "*" + checksum: 9578470db0b527c26e246a1220ae9bffc6bf47f20f89c54aac467c083ab1f7e16c00d9a7b4bb6cb4e2dfae465027270827e5908a6236063f6214625e50585d78 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 20.2.1 resolution: "@types/yargs-parser@npm:20.2.1" @@ -4002,6 +4028,15 @@ __metadata: languageName: node linkType: hard +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: ^1.0.0 + checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 + languageName: node + linkType: hard + "braces@npm:^3.0.1, braces@npm:~3.0.2": version: 3.0.2 resolution: "braces@npm:3.0.2" @@ -4529,6 +4564,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:9.0.0": + version: 9.0.0 + resolution: "commander@npm:9.0.0" + checksum: 15066e433d528315ded8261d16bc600d1f3c5671c75021e685ae67e4d62f7551243ff28411b28dc0a6f8b23c2a0f033550ec6f3e66bdf9d11a4fdc2d33dd9802 + languageName: node + linkType: hard + "commander@npm:^2.20.0, commander@npm:^2.7.1": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -5897,6 +5939,19 @@ __metadata: languageName: node linkType: hard +"folder-hash@npm:4.0.2": + version: 4.0.2 + resolution: "folder-hash@npm:4.0.2" + dependencies: + debug: ^4.3.3 + graceful-fs: ~4.2.9 + minimatch: ~5.0.0 + bin: + folder-hash: bin/folder-hash + checksum: c087a02f5c39e6869c72e40d196ee642a972e5219b83239ba61f2971c710b6fbee0e9bfbe834cdb59104a7a696596be75c4c3365fc1e7aae8bbe652a650c8092 + languageName: node + linkType: hard + "follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.4": version: 1.14.9 resolution: "follow-redirects@npm:1.14.9" @@ -6204,7 +6259,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.3, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.3, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9, graceful-fs@npm:~4.2.9": version: 4.2.9 resolution: "graceful-fs@npm:4.2.9" checksum: 68ea4e07ff2c041ada184f9278b830375f8e0b75154e3f080af6b70f66172fabb4108d19b3863a96b53fc068a310b9b6493d86d1291acc5f3861eb4b79d26ad6 @@ -8035,6 +8090,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:~5.0.0": + version: 5.0.0 + resolution: "minimatch@npm:5.0.0" + dependencies: + brace-expansion: ^2.0.1 + checksum: 810d4165fa2b16d0ffe8eb7586b8b7b7122ab77efa8f351686ffd1e8cbe63997ae9d5e7404fe611a676e59e1a47f7f5e8d9a004e0fe5bb6d5ac35457469116a4 + languageName: node + linkType: hard + "minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5": version: 1.2.5 resolution: "minimist@npm:1.2.5" @@ -8634,6 +8698,23 @@ __metadata: languageName: node linkType: hard +"ora-classic@npm:5.4.2": + version: 5.4.2 + resolution: "ora-classic@npm:5.4.2" + dependencies: + bl: ^4.1.0 + chalk: ^4.1.0 + cli-cursor: ^3.1.0 + cli-spinners: ^2.5.0 + is-interactive: ^1.0.0 + is-unicode-supported: ^0.1.0 + log-symbols: ^4.1.0 + strip-ansi: ^6.0.0 + wcwidth: ^1.0.1 + checksum: f52ec97c8fbb051dd883d6bbb75a0b658a173f8230aba0d39b2cf5957b79dcaf7da53dda72d744d726ac8cb7e693fe6d7eb0c27a59298aa35f0ec06651c4626f + languageName: node + linkType: hard + "ora@npm:^5.4.1": version: 5.4.1 resolution: "ora@npm:5.4.1" @@ -10001,16 +10082,29 @@ __metadata: version: 0.0.0-use.local resolution: "scripts@workspace:scripts" dependencies: + "@apidevtools/swagger-parser": 10.0.3 "@octokit/rest": 18.12.0 + "@types/folder-hash": 4.0.1 + "@types/inquirer": 8.2.0 + "@types/jest": 27.4.0 "@types/js-yaml": 4.0.5 + "@types/mustache": 4.1.2 "@types/node": 16.11.11 "@types/semver": 7.3.9 + commander: 9.0.0 dotenv: 16.0.0 + eslint: 8.6.0 execa: 5.1.1 + folder-hash: 4.0.2 + inquirer: 8.2.0 jest: 27.4.7 js-yaml: 4.1.0 + mustache: 4.2.0 + openapi-types: 10.0.0 + ora-classic: 5.4.2 semver: 7.3.5 ts-jest: 27.1.3 + ts-node: 10.5.0 typescript: 4.5.4 languageName: unknown linkType: soft @@ -10588,23 +10682,6 @@ __metadata: languageName: node linkType: hard -"tests@workspace:tests": - version: 0.0.0-use.local - resolution: "tests@workspace:tests" - dependencies: - "@apidevtools/swagger-parser": 10.0.3 - "@types/jest": 27.4.0 - "@types/mustache": 4.1.2 - "@types/node": 16.11.11 - eslint: 8.6.0 - jest: 27.4.7 - mustache: 4.2.0 - openapi-types: 10.0.0 - ts-jest: 27.1.3 - typescript: 4.5.4 - languageName: unknown - linkType: soft - "text-table@npm:^0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0"