diff --git a/packages/cli/lang/en.json b/packages/cli/lang/en.json index 601487ecee..b3c1473360 100644 --- a/packages/cli/lang/en.json +++ b/packages/cli/lang/en.json @@ -64,7 +64,7 @@ "commands_create_options_o": "Output directory for the new project", "commands_create_options_o_path": "path", "commands_create_options_projectName": "project-name", - "commands_create_options_recipeScript": "recipe-script", + "commands_query_options_recipeScript": "Path to recipe script", "commands_create_overwritePrompt": "Do you want to overwrite this directory?", "commands_create_overwriting": "Overwriting {dir}...", "commands_create_readyApp": "You are ready to build an app using Web3API", @@ -103,6 +103,7 @@ "commands_plugin_options_publish": "Output path for the built schema and manifest (default: {default})", "commands_plugin_options_codegen": "Output directory for the generated types (default: {default})", "commands_query_options_configPath": "config-path", + "commands_query_options_outputFilePath": "output-file-path", "commands_query_options_config": "Add custom configuration to the Web3ApiClient", "commands_query_options_outputFile": "Output file path for the query result", "commands_query_options_quiet": "Suppress output", @@ -138,6 +139,7 @@ "commands_query_error_missingScript": "Required argument {script} is missing", "commands_query_error_noApi": "API needs to be initialized", "commands_query_error_readFail": "Failed to read query {query}", + "commands_query_error_noRecipeScriptFound": "Recipe script not found at path: {path}", "commands_query_error_noTestEnvFound": "w3 test-env not found, please run 'w3 test-env up'", "commands_testEnv_description": "Manage a test environment for Web3API", "commands_testEnv_error_never": "This should never happen...", diff --git a/packages/cli/lang/es.json b/packages/cli/lang/es.json index 5b1a4a5611..b3c1473360 100644 --- a/packages/cli/lang/es.json +++ b/packages/cli/lang/es.json @@ -7,7 +7,6 @@ "commands_build_keypressListener_watching": "Watching", "commands_build_options_h": "Show usage information", "commands_build_options_m": "Path to the Web3API Build manifest file (default: {default})", - "commands_build_options_o": "Output directory for build results (default: build/)", "commands_build_options_o_path": "path", "commands_build_options_options": "options", @@ -65,7 +64,7 @@ "commands_create_options_o": "Output directory for the new project", "commands_create_options_o_path": "path", "commands_create_options_projectName": "project-name", - "commands_create_options_recipeScript": "recipe-script", + "commands_query_options_recipeScript": "Path to recipe script", "commands_create_overwritePrompt": "Do you want to overwrite this directory?", "commands_create_overwriting": "Overwriting {dir}...", "commands_create_readyApp": "You are ready to build an app using Web3API", @@ -104,6 +103,7 @@ "commands_plugin_options_publish": "Output path for the built schema and manifest (default: {default})", "commands_plugin_options_codegen": "Output directory for the generated types (default: {default})", "commands_query_options_configPath": "config-path", + "commands_query_options_outputFilePath": "output-file-path", "commands_query_options_config": "Add custom configuration to the Web3ApiClient", "commands_query_options_outputFile": "Output file path for the query result", "commands_query_options_quiet": "Suppress output", @@ -139,6 +139,7 @@ "commands_query_error_missingScript": "Required argument {script} is missing", "commands_query_error_noApi": "API needs to be initialized", "commands_query_error_readFail": "Failed to read query {query}", + "commands_query_error_noRecipeScriptFound": "Recipe script not found at path: {path}", "commands_query_error_noTestEnvFound": "w3 test-env not found, please run 'w3 test-env up'", "commands_testEnv_description": "Manage a test environment for Web3API", "commands_testEnv_error_never": "This should never happen...", @@ -194,14 +195,14 @@ "lib_helpers_manifest_loadText": "Manifest loaded from {path}", "lib_helpers_manifest_loadWarning": "Warnings loading manifest from {path}", "lib_helpers_deployManifestExt_loadError": "Failed to load deploy manifest extension from {path}", - "lib_helpers_deployManifestExt_loadText": "Deploy manifest extension loaded from {path}", - "lib_helpers_deployManifestExt_loadWarning": "No deploy manifest extension found in {path}", + "lib_helpers_deployManifestExt_loadText": "Load manifest extension from {path}", + "lib_helpers_deployManifestExt_loadWarning": "No manifest extension found in {path}", "lib_helpers_manifest_outputError": "Failed to output manifest to {path}", "lib_helpers_manifest_outputText": "Manifest written to {path}", "lib_helpers_manifest_outputWarning": "Warnings writing manifest to {path}", "lib_helpers_manifest_unableToDump": "Unable to dump manifest: {manifest}", "lib_helpers_manifest_unableToLoad": "Unable to load manifest: {path}", - "lib_helpers_docker_copyText": "Artifacts written to {path} from the image {image}", + "lib_helpers_docker_copyText": "Artifacts written to {path} from the image `{image}`", "lib_helpers_docker_copyError": "Failed to write build artifacts to {path} from the image `{image}`", "lib_helpers_docker_copyWarning": "Warnings write build artifacts to {path} from the image `{image}`", "lib_helpers_docker_buildText": "Building source image `{image}` using dockerfile `{dockerfile}` in context `{context}`", diff --git a/packages/cli/package.json b/packages/cli/package.json index c23a8d452f..56a5b7caba 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -20,6 +20,7 @@ "build:envs": "copyfiles ./src/lib/build-envs/**/**/* ./build/lib/build-envs/ -u 3", "build:deployers": "copyfiles ./src/lib/deployers/**/web3api.deploy.ext.json ./build/lib/deployers -u 3", "prebuild": "ts-node ./scripts/generateIntlTypes.ts", + "build:fast": "rimraf ./build && tsc --project tsconfig.build.json && yarn build:envs", "lint": "eslint --color -c ../../.eslintrc.js .", "test": "cross-env TEST=true jest --passWithNoTests --runInBand --verbose", "test:ci": "cross-env TEST=true jest --passWithNoTests --runInBand --verbose", @@ -46,6 +47,7 @@ "axios": "0.21.2", "chalk": "4.1.0", "chokidar": "3.5.1", + "commander": "9.2.0", "content-hash": "2.5.2", "copyfiles": "2.4.1", "ethers": "5.0.7", diff --git a/packages/cli/src/__tests__/e2e/app.spec.ts b/packages/cli/src/__tests__/e2e/app.spec.ts index 41622616a2..ded371d593 100644 --- a/packages/cli/src/__tests__/e2e/app.spec.ts +++ b/packages/cli/src/__tests__/e2e/app.spec.ts @@ -5,20 +5,18 @@ import { GetPathToCliTestFiles } from "@web3api/test-cases"; import path from "path"; import fs from "fs"; -const HELP = ` -w3 app command [options] -Commands: - codegen Generate code for the app +const HELP = `Usage: w3 app|a [options] [command] + +Build/generate types for your app Options: - -h, --help Show usage information - -m, --manifest-file Path to the Web3API App manifest file (default: web3api.app.yaml | web3api.app.yml) - -c, --codegen-dir Output directory for the generated code (default: ./src/w3) - -i, --ipfs [] IPFS node to load external schemas (default: ipfs.io & localhost) - -e, --ens [
] ENS address to lookup external schemas (default: 0x0000...2e1e) + -h, --help display help for command -`; +Commands: + codegen [options] Generate code for the app + help [command] display help for command +` const CODEGEN_SUCCESS = `- Manifest loaded from ./web3api.app.yaml ✔ Manifest loaded from ./web3api.app.yaml @@ -60,9 +58,8 @@ describe("e2e tests for app command", () => { ); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)).toEqual(`Please provide a command -${HELP}`); + expect(error).toBe("error: unknown option '--output-dir'\n"); + expect(output).toEqual(``); }); test("Should throw error for invalid params - codegen-dir", async () => { @@ -75,10 +72,8 @@ ${HELP}`); ); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)) - .toEqual(`--codegen-dir option missing argument -${HELP}`); + expect(error).toBe(`error: option '-c, --codegen-dir ' argument missing\n`); + expect(output).toEqual(``); }); test("Should throw error for invalid params - ens", async () => { @@ -91,10 +86,9 @@ ${HELP}`); ); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)) - .toEqual(`--ens option missing [
] argument -${HELP}`); + expect(error).toBe("error: option '-e, --ens [
]' argument missing\n"); + expect(output) + .toEqual(``); }); describe("test-cases", () => { diff --git a/packages/cli/src/__tests__/e2e/build.spec.ts b/packages/cli/src/__tests__/e2e/build.spec.ts index b93e40c277..74df1def1e 100644 --- a/packages/cli/src/__tests__/e2e/build.spec.ts +++ b/packages/cli/src/__tests__/e2e/build.spec.ts @@ -6,16 +6,19 @@ import { GetPathToCliTestFiles } from "@web3api/test-cases"; import fs from "fs"; import path from "path"; -const HELP = ` -w3 build [options] +const HELP = `Usage: w3 build|b [options] -Options: - -h, --help Show usage information - -m, --manifest-file Path to the Web3API Build manifest file (default: web3api.yaml | web3api.yml) - -o, --output-dir Output directory for build results (default: build/) - -w, --watch Automatically rebuild when changes are made (default: false) - -v, --verbose Verbose output (default: false) +Builds a Web3API +Options: + -m, --manifest-file Path to the Web3API Build manifest file (default: + web3api.yaml | web3api.yml) + -o, --output-dir Output directory for build results (default: + build/) + -w, --watch Automatically rebuild when changes are made + (default: false) + -v, --verbose Verbose output (default: false) + -h, --help display help for command `; jest.setTimeout(500000); @@ -53,10 +56,8 @@ describe("e2e tests for build command", () => { ); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)) - .toEqual(`--output-dir option missing argument -${HELP}`); + expect(error).toContain("error: option '-o, --output-dir ' argument missing"); + expect(output).toBe("") }); test("Adds uuid-v4 suffix to build-env image if no build manifest specified", async () => { diff --git a/packages/cli/src/__tests__/e2e/codegen.spec.ts b/packages/cli/src/__tests__/e2e/codegen.spec.ts index 0262707aa4..cbd0422cf5 100644 --- a/packages/cli/src/__tests__/e2e/codegen.spec.ts +++ b/packages/cli/src/__tests__/e2e/codegen.spec.ts @@ -5,28 +5,32 @@ import { runCLI } from "@web3api/test-env-js"; import { GetPathToCliTestFiles } from "@web3api/test-cases"; import path from "path"; import fs from "fs"; +import rimraf from "rimraf"; -const HELP = ` -w3 codegen [options] +const HELP = `Usage: w3 codegen|g [options] -Options: - -h, --help Show usage information - -m, --manifest-file Path to the Web3API manifest file (default: ${defaultWeb3ApiManifest.join( - " | " - )}) - -c, --codegen-dir Output directory for the generated code (default: ./w3) - -s, --script Path to a custom generation script (JavaScript | TypeScript) - -i, --ipfs [] IPFS node to load external schemas (default: ipfs.io & localhost) - -e, --ens [
] ENS address to lookup external schemas (default: 0x0000...2e1e) +Auto-generate API Types +Options: + -m, --manifest-file Path to the Web3API manifest file (default: + ${defaultWeb3ApiManifest.join(" | ")}) + -c, --codegen-dir Output directory for the generated code + (default: ./w3) + -s, --script Path to a custom generation script (JavaScript | + TypeScript) + -i, --ipfs [] IPFS node to load external schemas (default: + ipfs.io & localhost) + -e, --ens [
] ENS address to lookup external schemas (default: + 0x0000...2e1e) + -h, --help display help for command `; describe("e2e tests for codegen command", () => { const testCaseRoot = path.join(GetPathToCliTestFiles(), "api/codegen"); - const testCases = - fs.readdirSync(testCaseRoot, { withFileTypes: true }) - .filter((dirent) => dirent.isDirectory()) - .map((dirent) => dirent.name); + const testCases = fs + .readdirSync(testCaseRoot, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); const getTestCaseDir = (index: number) => path.join(testCaseRoot, testCases[index]); @@ -42,55 +46,75 @@ describe("e2e tests for codegen command", () => { expect(clearStyle(output)).toEqual(HELP); }); - test("Should throw error for invalid params - script", async () => { + test("Should throw error for invalid params - ens", async () => { const { exitCode: code, stdout: output, stderr: error } = await runCLI({ - args: ["codegen", "--script"], + args: ["codegen", "--ens"], cwd: getTestCaseDir(0), cli: w3Cli, }); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)) - .toEqual(`--script option missing argument -${HELP}`); + expect(error).toBe( + "error: option '-e, --ens [
]' argument missing\n" + ); + expect(clearStyle(output)).toEqual(``); }); - test("Should throw error for invalid params - ens", async () => { + test("Should throw error for invalid generation file - wrong file", async () => { const { exitCode: code, stdout: output, stderr: error } = await runCLI({ - args: ["codegen", "--ens"], + args: ["codegen", "--script", `web3api-invalid.gen.js`], cwd: getTestCaseDir(0), cli: w3Cli, }); + const genFile = path.normalize( + `${getTestCaseDir(0)}/web3api-invalid.gen.js` + ); + expect(code).toEqual(1); expect(error).toBe(""); - expect(clearStyle(output)) - .toEqual(`--ens option missing [
] argument -${HELP}`); + expect(clearStyle(output)).toContain( + `Failed to generate types: Cannot find module '${genFile}'` + ); }); - test("Should throw error for invalid generation file - wrong file", async () => { + test("Should throw error for invalid generation file - no run() method", async () => { const { exitCode: code, stdout: output, stderr: error } = await runCLI({ - args: ["codegen", "--script", `web3api-invalid.gen.js`], + args: ["codegen", "--script", `web3api-norun.gen.js`], cwd: getTestCaseDir(0), cli: w3Cli, }); - const genFile = path.normalize(`${getTestCaseDir(0)}/web3api-invalid.gen.js`); - expect(code).toEqual(1); expect(error).toBe(""); - expect(clearStyle(output)).toContain(`Failed to generate types: Cannot find module '${genFile}'`); + expect(clearStyle(output)).toContain( + `Failed to generate types: The generation file provided doesn't have the 'generateBinding' method.` + ); }); + test("Should successfully generate types", async () => { + rimraf.sync(`${getTestCaseDir(0)}/types`); + + const { exitCode: code, stdout: output, stderr: error } = await runCLI({ + args: ["codegen"], + cwd: getTestCaseDir(0), + cli: w3Cli, + }); + + expect(code).toEqual(0); + expect(error).toBe(""); + expect(clearStyle(output)).toContain( + `🔥 Types were generated successfully 🔥` + ); + + rimraf.sync(`${getTestCaseDir(0)}/types`); + }); describe("test-cases", () => { for (let i = 0; i < testCases.length; ++i) { const testCaseName = testCases[i]; const testCaseDir = getTestCaseDir(i); test(testCaseName, async () => { - let cmdArgs = []; let cmdFile = path.join(testCaseDir, "cmd.json"); if (fs.existsSync(cmdFile)) { @@ -100,20 +124,19 @@ ${HELP}`); } } - let { exitCode, stdout, stderr } = await runCLI( - { - args: ["codegen", ...cmdArgs], - cwd: testCaseDir, - cli: w3Cli, - }, - ); + let { exitCode, stdout, stderr } = await runCLI({ + args: ["codegen", ...cmdArgs], + cwd: testCaseDir, + cli: w3Cli, + }); stdout = clearStyle(stdout); stderr = clearStyle(stderr); const expected = JSON.parse( fs.readFileSync( - path.join(testCaseDir, "expected/output.json"), "utf-8" + path.join(testCaseDir, "expected/output.json"), + "utf-8" ) ); diff --git a/packages/cli/src/__tests__/e2e/create.spec.ts b/packages/cli/src/__tests__/e2e/create.spec.ts index 6a3103558e..cd950bef41 100644 --- a/packages/cli/src/__tests__/e2e/create.spec.ts +++ b/packages/cli/src/__tests__/e2e/create.spec.ts @@ -1,28 +1,25 @@ -import { supportedLangs } from "../../commands/create"; import { clearStyle, w3Cli } from "./utils"; import { runCLI } from "@web3api/test-env-js"; import rimraf from "rimraf"; -const HELP = ` -w3 create command [options] +const HELP = `Usage: w3 create|c [options] [command] -Commands: - api Create a Web3API project - langs: ${supportedLangs.api.join(", ")} - app Create a Web3API application - langs: ${supportedLangs.app.join(", ")} - plugin Create a Web3API plugin - langs: ${supportedLangs.plugin.join(", ")} +Create a new project with w3 CLI Options: - -h, --help Show usage information - -o, --output-dir Output directory for the new project + -h, --help display help for command +Commands: + api [options] Create a Web3API project langs: + assemblyscript, interface + app [options] Create a Web3API application langs: + typescript-node, typescript-react + plugin [options] Create a Web3API plugin langs: typescript + help [command] display help for command `; describe("e2e tests for create command", () => { - test("Should show help text", async () => { const { exitCode: code, stdout: output, stderr: error } = await runCLI({ args: ["create", "--help"], @@ -41,9 +38,8 @@ describe("e2e tests for create command", () => { }); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)).toEqual(`Please provide a command -${HELP}`); + expect(error).toBe(HELP); + expect(output).toBe(""); }); test("Should throw error for missing parameter - lang", async () => { @@ -53,9 +49,8 @@ ${HELP}`); }); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)).toEqual(`Please provide a language -${HELP}`); + expect(error).toContain("error: unknown command 'type'"); + expect(output).toBe(""); }); test("Should throw error for missing parameter - name", async () => { @@ -65,9 +60,8 @@ ${HELP}`); }); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)).toEqual(`Please provide a project name -${HELP}`); + expect(error).toContain("error: unknown command 'type'"); + expect(output).toBe(""); }); test("Should throw error for invalid parameter - type", async () => { @@ -77,9 +71,8 @@ ${HELP}`); }); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)).toEqual(`Unrecognized command "unknown" -${HELP}`); + expect(error).toContain("error: unknown command 'unknown'"); + expect(output).toBe(""); }); test("Should throw error for invalid parameter - lang", async () => { @@ -89,9 +82,8 @@ ${HELP}`); }); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)).toEqual(`Unrecognized language "unknown" -${HELP}`); + expect(error).toContain("error: command-argument value 'unknown' is invalid for argument 'language'. Allowed choices are assemblyscript, interface."); + expect(output).toBe(""); }); test("Should throw error for invalid parameter - output-dir", async () => { @@ -101,17 +93,22 @@ ${HELP}`); }); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)) - .toEqual(`--output-dir option missing argument -${HELP}`); + expect(error).toContain("error: option '-o, --output-dir ' argument missing"); + expect(output).toBe(""); }); test("Should successfully generate project", async () => { rimraf.sync(`${__dirname}/test`); const { exitCode: code, stdout: output } = await runCLI({ - args: ["create", "api", "assemblyscript", "test", "-o", `${__dirname}/test`], + args: [ + "create", + "api", + "assemblyscript", + "test", + "-o", + `${__dirname}/test`, + ], cwd: __dirname, cli: w3Cli, }); diff --git a/packages/cli/src/__tests__/e2e/deploy.spec.ts b/packages/cli/src/__tests__/e2e/deploy.spec.ts index b63e0fd391..94508fdbc7 100644 --- a/packages/cli/src/__tests__/e2e/deploy.spec.ts +++ b/packages/cli/src/__tests__/e2e/deploy.spec.ts @@ -15,13 +15,15 @@ import yaml from "js-yaml"; import path from "path"; import fs from "fs"; -const HELP = ` -w3 deploy [options] +const HELP = `Usage: w3 deploy|d [options] + +Deploys/Publishes a Web3API Options: - -h, --help Show usage information - -m, --manifest-file Path to the Web3API Deploy manifest file (default: web3api.yaml | web3api.yml) - -v, --verbose Verbose output (default: false) + -m, --manifest-file Path to the Web3API Deploy manifest file + (default: web3api.yaml | web3api.yml) + -v, --verbose Verbose output (default: false) + -h, --help display help for command `; const testCaseRoot = path.join(GetPathToCliTestFiles(), "api/deploy"); diff --git a/packages/cli/src/__tests__/e2e/help.spec.ts b/packages/cli/src/__tests__/e2e/help.spec.ts index e55a42f595..4915c79533 100644 --- a/packages/cli/src/__tests__/e2e/help.spec.ts +++ b/packages/cli/src/__tests__/e2e/help.spec.ts @@ -2,17 +2,21 @@ import { clearStyle, w3Cli } from "./utils"; import { runCLI } from "@web3api/test-env-js"; -const HELP = ` - w3 🔥 Web3API CLI 🔥 - help (h) - - test-env (t) Manage a test environment for Web3API - query (q) Query Web3APIs using recipe scripts - plugin (p) Build/generate types for the plugin - deploy (b) Deploys/Publishes a Web3API - create (c) Create a new project with w3 CLI - codegen (g) Auto-generate API Types - build (b) Builds a Web3API - app (a) Build/generate types for your app +const HELP = `Usage: w3 [options] [command] + +Options: + -h, --help display help for command + +Commands: + app|a Build/generate types for your app + build|b [options] Builds a Web3API + codegen|g [options] Auto-generate API Types + create|c Create a new project with w3 CLI + deploy|d [options] Deploys/Publishes a Web3API + plugin|p Build/generate types for the plugin + query|q [options] Query Web3APIs using recipe scripts + test-env|t Manage a test environment for Web3API + help [command] display help for command `; describe("e2e tests for no help", () => { diff --git a/packages/cli/src/__tests__/e2e/no-command.spec.ts b/packages/cli/src/__tests__/e2e/no-command.spec.ts index c0fb3ab50f..8cd248794b 100644 --- a/packages/cli/src/__tests__/e2e/no-command.spec.ts +++ b/packages/cli/src/__tests__/e2e/no-command.spec.ts @@ -2,6 +2,24 @@ import { clearStyle, w3Cli } from "./utils"; import { runCLI } from "@web3api/test-env-js"; + +const HELP = `Usage: w3 [options] [command] + +Options: + -h, --help display help for command + +Commands: + app|a Build/generate types for your app + build|b [options] Builds a Web3API + codegen|g [options] Auto-generate API Types + create|c Create a new project with w3 CLI + deploy|d [options] Deploys/Publishes a Web3API + plugin|p Build/generate types for the plugin + query|q [options] Query Web3APIs using recipe scripts + test-env|t Manage a test environment for Web3API + help [command] display help for command +`; + describe("e2e tests for no command", () => { test("Should throw error for unrecognized command", async () => { @@ -9,10 +27,9 @@ describe("e2e tests for no command", () => { args: ["unknown"], cli: w3Cli }); - - expect(code).toEqual(0); - expect(error).toBe(""); - expect(clearStyle(output)).toEqual(`w3 unknown is not a command\n`); + expect(code).toEqual(1); + expect(error).toBe(`error: unknown command 'unknown'\n`); + expect(output).toEqual(``); }); test("Should let the user to type w3 help", async () => { @@ -20,11 +37,8 @@ describe("e2e tests for no command", () => { args: [], cli: w3Cli, }); - - expect(code).toEqual(0); - expect(error).toBe(""); - expect(clearStyle(output)).toEqual( - `Type w3 help to view common commands\n` - ); + expect(code).toEqual(1); + expect(clearStyle(error)).toBe(clearStyle(HELP)); + expect(output).toEqual(``); }); -}); +}); \ No newline at end of file diff --git a/packages/cli/src/__tests__/e2e/plugin.spec.ts b/packages/cli/src/__tests__/e2e/plugin.spec.ts index 9ded1e492f..dccce3fb34 100644 --- a/packages/cli/src/__tests__/e2e/plugin.spec.ts +++ b/packages/cli/src/__tests__/e2e/plugin.spec.ts @@ -1,4 +1,3 @@ -import { defaultPluginManifest } from "../../lib"; import { clearStyle } from "./utils"; import { runCLI } from "@web3api/test-env-js"; @@ -7,22 +6,16 @@ import { compareSync } from "dir-compare"; import path from "path"; import fs from "fs"; -const HELP = ` -w3 plugin command [options] +const HELP = `Usage: w3 plugin|p [options] [command] -Commands: - codegen Generate code for the plugin +Build/generate types for the plugin Options: - -h, --help Show usage information - -m, --manifest-file Path to the Web3API Plugin manifest file (default: ${defaultPluginManifest.join( - " | " - )}) - -p, --publish-dir Output path for the built schema and manifest (default: ./build) - -c, --codegen-dir Output directory for the generated types (default: ./src/w3) - -i, --ipfs [] IPFS node to load external schemas (default: dev-server's node) - -e, --ens [
] ENS address to lookup external schemas (default: 0x0000...2e1e) + -h, --help display help for command +Commands: + codegen [options] + help [command] display help for command `; const CODEGEN_SUCCESS = `- Manifest loaded from ./web3api.plugin.yaml @@ -56,7 +49,7 @@ describe("e2e tests for plugin command", () => { }); test("Should throw error for invalid params - no command", async () => { - const { exitCode: code, stdout: output } = await runCLI( + const { exitCode: code, stderr: error, stdout: output } = await runCLI( { args: ["plugin", "--codegen-dir"], cwd: getTestCaseDir(0), @@ -64,7 +57,8 @@ describe("e2e tests for plugin command", () => { ); expect(code).toEqual(1); - expect(clearStyle(output)).toEqual("Please provide a command\n" + HELP); + expect(error).toContain("error: unknown option '--codegen-dir'"); + expect(output).toBe(""); }); test("Should throw error for invalid params - publish-dir", async () => { @@ -76,9 +70,8 @@ describe("e2e tests for plugin command", () => { ); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)) - .toEqual("--publish-dir option missing argument\n" + HELP); + expect(error).toContain("error: option '-p, --publish-dir ' argument missing"); + expect(output).toBe(""); }); test("Should throw error for invalid params - codegen-dir", async () => { @@ -90,10 +83,8 @@ describe("e2e tests for plugin command", () => { ); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)) - .toEqual(`--codegen-dir option missing argument -${HELP}`); + expect(error).toContain("error: option '-c, --codegen-dir ' argument missing"); + expect(output).toBe(""); }); test("Should throw error for invalid params - ens", async () => { @@ -105,10 +96,8 @@ ${HELP}`); ); expect(code).toEqual(1); - expect(error).toBe(""); - expect(clearStyle(output)) - .toEqual(`--ens option missing [
] argument -${HELP}`); + expect(error).toContain("error: option '-e, --ens [
]' argument missing"); + expect(output).toBe(""); }); describe("test-cases", () => { diff --git a/packages/cli/src/__tests__/e2e/query.spec.ts b/packages/cli/src/__tests__/e2e/query.spec.ts index 09bfcaff78..d7efec3074 100644 --- a/packages/cli/src/__tests__/e2e/query.spec.ts +++ b/packages/cli/src/__tests__/e2e/query.spec.ts @@ -4,7 +4,11 @@ import yaml from "js-yaml"; import { clearStyle, w3Cli } from "./utils"; -import { buildAndDeployApi, initTestEnvironment, runCLI } from "@web3api/test-env-js"; +import { + buildAndDeployApi, + initTestEnvironment, + runCLI, +} from "@web3api/test-env-js"; import { GetPathToCliTestFiles } from "@web3api/test-cases"; import { normalizeLineEndings } from "@web3api/os-js"; import { @@ -16,17 +20,62 @@ import { jest.setTimeout(200000); -const HELP = ` -w3 query [options] +const HELP = `Usage: w3 query|q [options] -Options: - -h, --help Show usage information - -c, --client-config Add custom configuration to the Web3ApiClient - -o, --output-file Output file path for the query result - -q, --quiet Suppress output +Query Web3APIs using recipe scripts +Arguments: + recipe Path to recipe script + +Options: + -c, --client-config Add custom configuration to the + Web3ApiClient + -o, --output-file Output file path for the query result + -q, --quiet Suppress output + -h, --help display help for command `; +describe("sanity tests for query command", () => { + const testCaseRoot = path.join(GetPathToCliTestFiles(), "api/query"); + + test("Should show help text", async () => { + const { exitCode: code, stdout: output, stderr: error } = await runCLI({ + args: ["query", "--help"], + cwd: testCaseRoot, + }); + + expect(code).toEqual(0); + expect(error).toBe(""); + expect(clearStyle(output)).toEqual(HELP); + }); + + test("Should throw error for missing recipe-string", async () => { + const { exitCode, stdout, stderr } = await runCLI({ + args: ["query"], + cwd: testCaseRoot, + cli: w3Cli, + }); + + expect(exitCode).toEqual(1); + expect(stderr).toContain("error: missing required argument 'recipe"); + expect(stdout).toEqual(``); + }); + + test("Should throw error is --client-config doesn't contain arguments", async () => { + const { exitCode, stdout, stderr } = await runCLI({ + args: ["query", "./recipes/e2e.json", "--client-config"], + cwd: testCaseRoot, + cli: w3Cli, + }); + + expect(exitCode).toEqual(1); + expect(stderr).toBe( + "error: option '-c, --client-config ' argument missing\n" + ); + expect(stdout).toEqual(``); + }); +}); + describe("e2e tests for query command", () => { const testCaseRoot = path.join(GetPathToCliTestFiles(), "api/query"); @@ -36,7 +85,7 @@ describe("e2e tests for query command", () => { ethereum, ensAddress: ens, registrarAddress, - resolverAddress + resolverAddress, } = await initTestEnvironment(); const { stderr: deployErr } = await runCLI({ @@ -66,43 +115,6 @@ describe("e2e tests for query command", () => { }); }); - test("Should output help text", async () => { - const { exitCode, stdout, stderr } = await runCLI({ - args: ["query", "--help"], - cli: w3Cli, - }); - - expect(exitCode).toEqual(0); - expect(stderr).toBe(""); - expect(clearStyle(stdout)).toEqual(HELP); - }); - - test("Should throw error for missing recipe-string", async () => { - const { exitCode, stdout, stderr } = await runCLI({ - args: ["query"], - cli: w3Cli, - }); - - expect(exitCode).toEqual(0); - expect(stderr).toBe(""); - expect(clearStyle(stdout)) - .toEqual(`Required argument is missing -${HELP}`); - }); - - test("Should throw error is --client-config doesn't contain arguments", async () => { - const { exitCode, stdout, stderr } = await runCLI({ - args: ["query", "./recipes/e2e.json", "--client-config"], - cli: w3Cli, - }); - - expect(exitCode).toEqual(0); - expect(stderr).toBe(""); - expect(clearStyle(stdout)) - .toEqual(`--client-config option missing argument -${HELP}`); - }); - test("Should successfully return response: using json recipes", async () => { const { exitCode: code, stdout: output, stderr: queryErr } = await runCLI({ args: ["query", "./recipes/e2e.json"], @@ -184,12 +196,12 @@ ${HELP}`); }); expect(fs.existsSync(`${testCaseRoot}/recipes/output.json`)).toBeTruthy(); - const arr: Array = JSON.parse(fs.readFileSync(`${testCaseRoot}/recipes/output.json`, "utf8")) + const arr: Array = JSON.parse( + fs.readFileSync(`${testCaseRoot}/recipes/output.json`, "utf8") + ); expect(Array.isArray(arr)).toBeTruthy(); - expect( - arr[2] - ).toMatchObject(getSampleObjectOutput()); + expect(arr[2]).toMatchObject(getSampleObjectOutput()); fs.unlinkSync(`${testCaseRoot}/recipes/output.json`); }, 48000); @@ -221,13 +233,11 @@ ${HELP}`); }); expect(fs.existsSync(`${testCaseRoot}/recipes/output.yaml`)).toBeTruthy(); - const arr: Array = yaml.load( + const arr: Array = (yaml.load( fs.readFileSync(`${testCaseRoot}/recipes/output.yaml`, "utf8") - ) as unknown as Array; + ) as unknown) as Array; expect(Array.isArray(arr)).toBeTruthy(); - expect( - arr[2] - ).toMatchObject(getSampleObjectOutput()); + expect(arr[2]).toMatchObject(getSampleObjectOutput()); fs.unlinkSync(`${testCaseRoot}/recipes/output.yaml`); }, 48000); @@ -249,7 +259,12 @@ ${HELP}`); }, 48000); test("Should use custom config for client if specified", async () => { - const configs = ["./client-config.ts", "./client-config.js", "./client-async-config.ts", "./client-async-config.js"]; + const configs = [ + "./client-config.ts", + "./client-config.js", + "./client-async-config.ts", + "./client-async-config.js", + ]; for (const config of configs) { const { exitCode, stdout, stderr } = await runCLI({ diff --git a/packages/cli/src/__tests__/e2e/test-env.spec.ts b/packages/cli/src/__tests__/e2e/test-env.spec.ts index 30d7a23ecf..e09b966b55 100644 --- a/packages/cli/src/__tests__/e2e/test-env.spec.ts +++ b/packages/cli/src/__tests__/e2e/test-env.spec.ts @@ -2,16 +2,17 @@ import { clearStyle, w3Cli } from "./utils"; import { runCLI } from "@web3api/test-env-js"; -const HELP = ` -w3 test-env command +const HELP = `Usage: w3 test-env|t [options] [command] -Commands: - up Startup the test env - down Shutdown the test env +Manage a test environment for Web3API Options: - -h, --help Show usage information + -h, --help display help for command +Commands: + up Startup the test env + down Shutdown the test env + help [command] display help for command `; describe("e2e tests for test-env command", () => { @@ -32,10 +33,9 @@ describe("e2e tests for test-env command", () => { cli: w3Cli, }); - expect(code).toEqual(0); - expect(error).toBe(""); - expect(clearStyle(output)).toEqual(`No command given -${HELP}`); + expect(code).toEqual(1); + expect(error).toBe(HELP); + expect(output).toBe(""); }); test("Should throw error for unrecognized command", async () => { @@ -44,10 +44,9 @@ ${HELP}`); cli: w3Cli, }); - expect(code).toEqual(0); - expect(error).toBe(""); - expect(clearStyle(output)).toEqual(`Unrecognized command: unknown -${HELP}`); + expect(code).toEqual(1); + expect(error).toContain("error: unknown command 'unknown'"); + expect(output).toBe(""); }); test("Should successfully start test environment", async () => { @@ -58,7 +57,7 @@ ${HELP}`); expect(code).toEqual(0); expect(error).toBe(""); - expect(clearStyle(output)).toContain(`- Starting test environment...`); + expect(clearStyle(output)).toContain(`Starting test environment...`); await runCLI({ args: ["test-env", "down"], @@ -74,6 +73,6 @@ ${HELP}`); expect(code).toEqual(0); expect(error).toBe(""); - expect(clearStyle(output)).toContain(`- Shutting down test environment...`); + expect(clearStyle(output)).toContain(`Shutting down test environment...`); }, 20000); }); diff --git a/packages/cli/src/__tests__/project/recipes/constants.json b/packages/cli/src/__tests__/project/recipes/constants.json new file mode 100644 index 0000000000..64341d86d9 --- /dev/null +++ b/packages/cli/src/__tests__/project/recipes/constants.json @@ -0,0 +1,3 @@ +{ + "SimpleStorageAddr": "0x58F132FBB86E21545A4Bace3C19f1C05d86d7A22" +} \ No newline at end of file diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 64a66b2ff9..079291258d 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,20 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { build, GluegunToolbox } from "gluegun"; +import * as Commands from "./commands"; +import { Command } from "./commands/types"; -interface Args { - [key: string]: unknown; -} +import { executeMaybeAsyncFunction } from "@web3api/core-js"; +import { program } from "commander"; -export const run = async (argv: Args): Promise => { - const cli = build("w3") - .src(__dirname) - .plugins(`${process.cwd()}/node_modules`, { - matching: "w3-*", - hidden: true, - }) - .help() - .create(); +export const run = async (argv: string[]): Promise => { + for (const command of Object.values(Commands) as Command[]) { + await executeMaybeAsyncFunction(command.setup, program); + } - return await cli.run(argv); + program.parse(argv); }; diff --git a/packages/cli/src/commands/app.ts b/packages/cli/src/commands/app.ts index 6e1e896e16..48b08f5feb 100644 --- a/packages/cli/src/commands/app.ts +++ b/packages/cli/src/commands/app.ts @@ -1,210 +1,106 @@ -/* eslint-disable prefer-const */ +import { Command, Program } from "./types"; import { AppProject, CodeGenerator, SchemaComposer, intlMsg, - fixParameters, - resolvePathIfExists, - defaultAppManifest, getSimpleClient, getTestEnvProviders, } from "../lib"; +import { + parseAppManifestFileOption, + parseAppCodegenDirOption, +} from "../lib/parsers"; -import chalk from "chalk"; -import { GluegunToolbox, GluegunPrint } from "gluegun"; import { Web3ApiClient } from "@web3api/client-js"; import * as path from "path"; -const commands = ["codegen"]; const defaultOutputTypesDir = "./src/w3"; -const cmdStr = intlMsg.commands_app_options_command(); -const optionsStr = intlMsg.commands_options_options(); -const codegenStr = intlMsg.commands_app_codegen(); -const defaultManifestStr = defaultAppManifest.join(" | "); -const nodeStr = intlMsg.commands_codegen_options_i_node(); -const pathStr = intlMsg.commands_codegen_options_o_path(); -const addrStr = intlMsg.commands_codegen_options_e_address(); - -const HELP = ` -${chalk.bold("w3 app")} ${cmdStr} [${optionsStr}] - -Commands: - ${chalk.bold("codegen")} ${codegenStr} - -Options: - -h, --help ${intlMsg.commands_codegen_options_h()} - -m, --manifest-file <${pathStr}> ${intlMsg.commands_app_options_m( - { - default: defaultManifestStr, - } -)} - -c, --codegen-dir <${pathStr}> ${intlMsg.commands_app_options_codegen( - { - default: defaultOutputTypesDir, - } -)} - -i, --ipfs [<${nodeStr}>] ${intlMsg.commands_codegen_options_i()} - -e, --ens [<${addrStr}>] ${intlMsg.commands_codegen_options_e()} -`; - -export default { - alias: ["a"], - description: intlMsg.commands_app_description(), - run: async (toolbox: GluegunToolbox): Promise => { - const { filesystem, parameters, print } = toolbox; - - // Options - let { help, manifestFile, codegenDir, ipfs, ens } = parameters.options; - const { h, m, c, i, e } = parameters.options; - - help = help || h; - manifestFile = manifestFile || m; - codegenDir = codegenDir || c; - ipfs = ipfs || i; - ens = ens || e; - - let command = ""; - try { - const params = parameters; - [command] = fixParameters( - { - options: params.options, - array: params.array, - }, - { - h, - help, - } - ); - } catch (e) { - print.error(e.message); - process.exitCode = 1; - return; - } - - if (help) { - print.info(HELP); - return; - } - - // Validate Params - const paramsValid = validateAppParams( - print, - command, - codegenDir, - ipfs, - ens - ); - - if (!paramsValid) { - print.info(HELP); - process.exitCode = 1; - return; - } - // Resolve manifest - const manifestPaths = manifestFile ? [manifestFile] : defaultAppManifest; - manifestFile = resolvePathIfExists(filesystem, manifestPaths); +type AppCommandOptions = { + manifestFile: string; + codegenDir: string; + ipfs?: string; + ens?: string; +}; - if (!manifestFile) { - print.error( - intlMsg.commands_app_error_manifestNotFound({ - paths: manifestPaths.join(", "), +export const app: Command = { + setup: (program: Program) => { + const appCommand = program + .command("app") + .alias("a") + .description(intlMsg.commands_app_description()); + + appCommand + .command("codegen") + .description(intlMsg.commands_app_codegen()) + .option( + `-m, --manifest-file <${intlMsg.commands_codegen_options_o_path()}>`, + intlMsg.commands_app_options_codegen({ + default: defaultOutputTypesDir, }) - ); - return; - } - - // Get providers and client - const { ipfsProvider, ethProvider } = await getTestEnvProviders(ipfs); - const ensAddress: string | undefined = ens; - const client: Web3ApiClient = getSimpleClient({ - ensAddress, - ethProvider, - ipfsProvider, - }); - - // App project - const project = new AppProject({ - rootCacheDir: path.dirname(manifestFile), - appManifestPath: manifestFile, - client, - }); - await project.validate(); - - if (codegenDir) { - codegenDir = filesystem.resolve(codegenDir); - } else { - codegenDir = filesystem.resolve(defaultOutputTypesDir); - } - - const schemaComposer = new SchemaComposer({ - project, - client, - }); - const codeGenerator = new CodeGenerator({ - project, - schemaComposer, - outputDir: codegenDir, - }); - - if (await codeGenerator.generate()) { - print.success(`🔥 ${intlMsg.commands_app_success()} 🔥`); - process.exitCode = 0; - } else { - process.exitCode = 1; - } + ) + .option( + `-c, --codegen-dir <${intlMsg.commands_codegen_options_o_path()}>`, + `${intlMsg.commands_app_options_codegen({ + default: defaultOutputTypesDir, + })}` + ) + .option( + `-i, --ipfs [<${intlMsg.commands_codegen_options_i_node()}>] `, + `${intlMsg.commands_codegen_options_i()}` + ) + .option( + `-e, --ens [<${intlMsg.commands_codegen_options_e_address()}>]`, + `${intlMsg.commands_codegen_options_e()}` + ) + .action(async (options) => { + await run({ + ...options, + manifestFile: parseAppManifestFileOption( + options.manifestFile, + undefined + ), + codegenDir: parseAppCodegenDirOption(options.codegenDir, undefined), + }); + }); }, }; -function validateAppParams( - print: GluegunPrint, - command: unknown, - codegenDir: unknown, - ipfs: unknown, - ens: unknown -): boolean { - if (!command || typeof command !== "string") { - print.error(intlMsg.commands_app_error_noCommand()); - return false; - } else if (commands.indexOf(command) === -1) { - print.error(intlMsg.commands_app_error_unknownCommand({ command })); - return false; +async function run(options: AppCommandOptions) { + const { manifestFile, codegenDir, ipfs, ens } = options; + + // Get providers and client + const { ipfsProvider, ethProvider } = await getTestEnvProviders(ipfs); + const ensAddress: string | undefined = ens; + const client: Web3ApiClient = getSimpleClient({ + ensAddress, + ethProvider, + ipfsProvider, + }); + + // App project + const project = new AppProject({ + rootCacheDir: path.dirname(manifestFile), + appManifestPath: manifestFile, + client, + }); + await project.validate(); + + const schemaComposer = new SchemaComposer({ + project, + client, + }); + const codeGenerator = new CodeGenerator({ + project, + schemaComposer, + outputDir: codegenDir, + }); + + if (await codeGenerator.generate()) { + console.log(`🔥 ${intlMsg.commands_app_success()} 🔥`); + process.exitCode = 0; + } else { + process.exitCode = 1; } - - if (codegenDir === true) { - const codegenDirMissingPathMessage = intlMsg.commands_app_error_optionMissingArgument( - { - option: "--codegen-dir", - argument: `<${pathStr}>`, - } - ); - print.error(codegenDirMissingPathMessage); - return false; - } - - if (ipfs === true) { - const ipfsMissingMessage = intlMsg.commands_app_error_optionMissingArgument( - { - option: "--ipfs", - argument: `[<${nodeStr}>]`, - } - ); - print.error(ipfsMissingMessage); - return false; - } - - if (ens === true) { - const ensAddressMissingMessage = intlMsg.commands_app_error_optionMissingArgument( - { - option: "--ens", - argument: `[<${addrStr}>]`, - } - ); - print.error(ensAddressMissingMessage); - return false; - } - - return true; } diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index ce0d4e546d..1bb254f04d 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,4 +1,4 @@ -/* eslint-disable prefer-const */ +import { Command, Program } from "./types"; import { Compiler, Web3ApiProject, @@ -8,216 +8,166 @@ import { watchEventName, intlMsg, defaultWeb3ApiManifest, - resolvePathIfExists, isDockerInstalled, FileLock, } from "../lib"; +import { + parseWasmManifestFileOption, + parseBuildOutputDirOption, +} from "../lib/parsers"; -import chalk from "chalk"; +import { print } from "gluegun"; import path from "path"; import readline from "readline"; -import { GluegunToolbox, GluegunPrint } from "gluegun"; const defaultManifestStr = defaultWeb3ApiManifest.join(" | "); -const defaultOutputDirectory = "./build"; -const optionsStr = intlMsg.commands_build_options_options(); const pathStr = intlMsg.commands_build_options_o_path(); -const HELP = ` -${chalk.bold("w3 build")} [${optionsStr}] - -${optionsStr[0].toUpperCase() + optionsStr.slice(1)}: - -h, --help ${intlMsg.commands_build_options_h()} - -m, --manifest-file <${pathStr}> ${intlMsg.commands_build_options_m({ - default: defaultManifestStr, -})} - -o, --output-dir <${pathStr}> ${intlMsg.commands_build_options_o()} - -w, --watch ${intlMsg.commands_build_options_w()} - -v, --verbose ${intlMsg.commands_build_options_v()} -`; - -export default { - alias: ["b"], - description: intlMsg.commands_build_description(), - run: async (toolbox: GluegunToolbox): Promise => { - const { filesystem, parameters, print } = toolbox; - - // Options - const { h, m, o, w, v } = parameters.options; - let { help, manifestFile, outputDir, watch, verbose } = parameters.options; - - help = help || h; - manifestFile = manifestFile || m; - outputDir = outputDir || o; - watch = watch || w; - verbose = verbose || v; - - // Validate Params - const paramsValid = validateBuildParams(print, manifestFile, outputDir); - - if (help || !paramsValid) { - print.info(HELP); - if (!paramsValid) { - process.exitCode = 1; - } - return; - } - - // Ensure docker is installed - if (!isDockerInstalled()) { - print.error(intlMsg.lib_docker_noInstall()); - return; - } - - // Resolve manifest & output directory - const manifestPaths = manifestFile - ? [manifestFile as string] - : defaultWeb3ApiManifest; - manifestFile = resolvePathIfExists(filesystem, manifestPaths); +type BuildCommandOptions = { + manifestFile: string; + outputDir: string; + watch?: boolean; + verbose?: boolean; +}; - if (!manifestFile) { - print.error( - intlMsg.commands_build_error_manifestNotFound({ - paths: manifestPaths.join(", "), +export const build: Command = { + setup: (program: Program) => { + program + .command("build") + .alias("b") + .description(intlMsg.commands_build_description()) + .option( + `-m, --manifest-file <${pathStr}>`, + intlMsg.commands_build_options_m({ + default: defaultManifestStr, }) - ); - return; - } + ) + .option( + `-o, --output-dir <${pathStr}>`, + `${intlMsg.commands_build_options_o()}` + ) + .option(`-w, --watch`, `${intlMsg.commands_build_options_w()}`) + .option(`-v, --verbose`, `${intlMsg.commands_build_options_v()}`) + .action(async (options) => { + await run({ + ...options, + manifestFile: parseWasmManifestFileOption( + options.manifestFile, + undefined + ), + outputDir: parseBuildOutputDirOption(options.outputDir, undefined), + }); + }); + }, +}; - outputDir = - (outputDir && filesystem.resolve(outputDir)) || - filesystem.path(defaultOutputDirectory); +async function run(options: BuildCommandOptions) { + const { watch, verbose, manifestFile, outputDir } = options; - const project = new Web3ApiProject({ - rootCacheDir: path.dirname(manifestFile), - web3apiManifestPath: manifestFile, - quiet: verbose ? false : true, - }); - await project.validate(); + // Ensure docker is installed + if (!isDockerInstalled()) { + console.log(intlMsg.lib_docker_noInstall()); + return; + } - // Aquire a project specific lock file for the docker service - const dockerLock = new FileLock( - project.getCachePath("build/DOCKER_LOCK"), - print.error - ); + const project = new Web3ApiProject({ + rootCacheDir: path.dirname(manifestFile), + web3apiManifestPath: manifestFile, + quiet: verbose ? false : true, + }); + await project.validate(); + + const dockerLock = new FileLock( + project.getCachePath("build/DOCKER_LOCK"), + print.error + ); + + const schemaComposer = new SchemaComposer({ + project, + }); + + const compiler = new Compiler({ + project, + outputDir, + schemaComposer, + }); + + const execute = async (): Promise => { + compiler.reset(); + const result = await compiler.compile(); + + if (!result) { + return result; + } - const schemaComposer = new SchemaComposer({ - project, - }); + return true; + }; - const compiler = new Compiler({ - project, - outputDir, - schemaComposer, - }); + if (!watch) { + await dockerLock.request(); + const result = await execute(); + await dockerLock.release(); - const execute = async (): Promise => { - compiler.reset(); - const result = await compiler.compile(); + if (!result) { + process.exitCode = 1; + return; + } + } else { + // Execute + await dockerLock.request(); + await execute(); + await dockerLock.release(); + + const keyPressListener = () => { + // Watch for escape key presses + console.log( + `${intlMsg.commands_build_keypressListener_watching()}: ${project.getManifestDir()}` + ); + console.log(intlMsg.commands_build_keypressListener_exit()); + readline.emitKeypressEvents(process.stdin); + process.stdin.on("keypress", async (str, key) => { + if ( + key.name == "escape" || + key.name == "q" || + (key.name == "c" && key.ctrl) + ) { + await watcher.stop(); + await dockerLock.release(); + process.kill(process.pid, "SIGINT"); + } + }); - if (!result) { - return result; + if (process.stdin.setRawMode) { + process.stdin.setRawMode(true); } - return true; + process.stdin.resume(); }; - if (!watch) { - await dockerLock.request(); - const result = await execute(); - await dockerLock.release(); + keyPressListener(); - if (!result) { - process.exitCode = 1; - return; - } - } else { - // Execute - await dockerLock.request(); - await execute(); - await dockerLock.release(); - - const keyPressListener = () => { - // Watch for escape key presses - print.info( - `${intlMsg.commands_build_keypressListener_watching()}: ${project.getManifestDir()}` - ); - print.info(intlMsg.commands_build_keypressListener_exit()); - readline.emitKeypressEvents(process.stdin); - process.stdin.on("keypress", async (str, key) => { - if ( - key.name == "escape" || - key.name == "q" || - (key.name == "c" && key.ctrl) - ) { - await watcher.stop(); - await dockerLock.release(); - process.kill(process.pid, "SIGINT"); - } - }); + // Watch the directory + const watcher = new Watcher(); - if (process.stdin.setRawMode) { - process.stdin.setRawMode(true); + watcher.start(project.getManifestDir(), { + ignored: [outputDir + "/**", project.getManifestDir() + "/**/w3/**"], + ignoreInitial: true, + execute: async (events: WatchEvent[]) => { + // Log all of the events encountered + for (const event of events) { + console.log(`${watchEventName(event.type)}: ${event.path}`); } - process.stdin.resume(); - }; - - keyPressListener(); - - // Watch the directory - const watcher = new Watcher(); - - watcher.start(project.getManifestDir(), { - ignored: [outputDir + "/**", project.getManifestDir() + "/**/w3/**"], - ignoreInitial: true, - execute: async (events: WatchEvent[]) => { - // Log all of the events encountered - for (const event of events) { - print.info(`${watchEventName(event.type)}: ${event.path}`); - } - - // Execute the build - await dockerLock.request(); - await execute(); - await dockerLock.release(); - - // Process key presses - keyPressListener(); - }, - }); - } - - process.exitCode = 0; - }, -}; + // Execute the build + await dockerLock.request(); + await execute(); + await dockerLock.release(); -function validateBuildParams( - print: GluegunPrint, - manifestFile: unknown, - outputDir: unknown -): boolean { - if (manifestFile === true) { - const manifestPathMissingMessage = intlMsg.commands_build_error_manifestPathMissing( - { - option: "--manifest-file", - argument: `<${pathStr}>`, - } - ); - print.error(manifestPathMissingMessage); - return false; - } - - if (outputDir === true) { - const outputDirMissingPathMessage = intlMsg.commands_build_error_outputDirMissingPath( - { - option: "--output-dir", - argument: `<${pathStr}>`, - } - ); - print.error(outputDirMissingPathMessage); - return false; + // Process key presses + keyPressListener(); + }, + }); } - return true; + process.exitCode = 0; } diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index f8061b8b5f..eef5cae384 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -1,4 +1,4 @@ -/* eslint-disable prefer-const */ +import { Command, Program } from "./types"; import { CodeGenerator, Compiler, @@ -7,190 +7,116 @@ import { intlMsg, defaultWeb3ApiManifest, getTestEnvProviders, - resolvePathIfExists, } from "../lib"; +import { + parseCodegenDirOption, + parseCodegenScriptOption, + parseWasmManifestFileOption, +} from "../lib/parsers"; -import chalk from "chalk"; import path from "path"; -import { GluegunToolbox, GluegunPrint } from "gluegun"; +import { filesystem } from "gluegun"; const defaultCodegenDir = "./w3"; -const optionsStr = intlMsg.commands_options_options(); const nodeStr = intlMsg.commands_codegen_options_i_node(); const pathStr = intlMsg.commands_codegen_options_o_path(); const addrStr = intlMsg.commands_codegen_options_e_address(); const defaultManifestStr = defaultWeb3ApiManifest.join(" | "); -const HELP = ` -${chalk.bold("w3 codegen")} [${optionsStr}] - -${optionsStr[0].toUpperCase() + optionsStr.slice(1)}: - -h, --help ${intlMsg.commands_codegen_options_h()} - -m, --manifest-file <${pathStr}> ${intlMsg.commands_codegen_options_m( - { - default: defaultManifestStr, - } -)} - -c, --codegen-dir <${pathStr}> ${intlMsg.commands_codegen_options_codegen( - { - default: defaultCodegenDir, - } -)} - -s, --script <${pathStr}> ${intlMsg.commands_codegen_options_s()} - -i, --ipfs [<${nodeStr}>] ${intlMsg.commands_codegen_options_i()} - -e, --ens [<${addrStr}>] ${intlMsg.commands_codegen_options_e()} -`; - -export default { - alias: ["g"], - description: intlMsg.commands_codegen_description(), - run: async (toolbox: GluegunToolbox): Promise => { - const { filesystem, parameters, print } = toolbox; - - // Options - const { h, m, c, s, i, e } = parameters.options; - let { - help, - manifestFile, - codegenDir, - script, - ipfs, - ens, - } = parameters.options; - - help = help || h; - manifestFile = manifestFile || m; - codegenDir = codegenDir || c; - script = script || s; - ipfs = ipfs || i; - ens = ens || e; - - // Validate Params - const paramsValid = validateCodegenParams( - print, - codegenDir, - (dir: string) => (codegenDir = dir), - script, - ipfs, - ens - ); - - if (help || !paramsValid) { - print.info(HELP); - if (!paramsValid) { - process.exitCode = 1; - } - return; - } - - const { ipfsProvider, ethProvider } = await getTestEnvProviders(ipfs); - const ensAddress: string | undefined = ens; - - // Resolve manifest file - manifestFile = resolvePathIfExists( - filesystem, - manifestFile ? [manifestFile] : defaultWeb3ApiManifest - ); - codegenDir = codegenDir && filesystem.resolve(codegenDir); - script = script && filesystem.resolve(script); - - // Web3Api Project - const project = new Web3ApiProject({ - rootCacheDir: path.dirname(manifestFile), - web3apiManifestPath: manifestFile, - }); - await project.validate(); - - const schemaComposer = new SchemaComposer({ - project, - ipfsProvider, - ethProvider, - ensAddress, - }); - - let result = false; - - if (script) { - const codeGenerator = new CodeGenerator({ - project, - schemaComposer, - customScript: script, - outputDir: codegenDir, - }); +type CodegenCommandOptions = { + manifestFile: string; + codegenDir: string; + script?: string; + ipfs?: string; + ens?: string; +}; - result = await codeGenerator.generate(); - } else { - const compiler = new Compiler({ - project, - outputDir: filesystem.path("build"), - schemaComposer, +export const codegen: Command = { + setup: (program: Program) => { + program + .command("codegen") + .alias("g") + .description(intlMsg.commands_codegen_description()) + .option( + `-m, --manifest-file <${pathStr}>`, + `${intlMsg.commands_codegen_options_m({ + default: defaultManifestStr, + })}` + ) + .option( + `-c, --codegen-dir <${pathStr}>`, + ` ${intlMsg.commands_codegen_options_codegen({ + default: defaultCodegenDir, + })}` + ) + .option( + `-s, --script <${pathStr}>`, + `${intlMsg.commands_codegen_options_s()}` + ) + .option( + `-i, --ipfs [<${nodeStr}>]`, + `${intlMsg.commands_codegen_options_i()}` + ) + .option( + `-e, --ens [<${addrStr}>]`, + `${intlMsg.commands_codegen_options_e()}` + ) + .action(async (options) => { + await run({ + ...options, + codegenDir: parseCodegenDirOption(options.codegenDir, undefined), + script: parseCodegenScriptOption(options.script, undefined), + manifestFile: parseWasmManifestFileOption( + options.manifestFile, + undefined + ), + }); }); - - result = await compiler.codegen(); - } - - if (result) { - print.success(`🔥 ${intlMsg.commands_codegen_success()} 🔥`); - process.exitCode = 0; - } else { - process.exitCode = 1; - } }, }; -function validateCodegenParams( - print: GluegunPrint, - codegenDir: unknown, - setCodegenDir: (dir: string) => void, - script: unknown, - ipfs: unknown, - ens: unknown -): boolean { - if (codegenDir === true) { - const codegenDirMessage = intlMsg.commands_codegen_error_optionMissingArgument( - { - option: "--codegen-dir", - argument: `<${pathStr}>`, - } - ); - print.error(codegenDirMessage); - return false; - } else if (!codegenDir) { - setCodegenDir(defaultCodegenDir); - } +async function run(options: CodegenCommandOptions) { + const { ipfs, ens, manifestFile, codegenDir, script } = options; + const { ipfsProvider, ethProvider } = await getTestEnvProviders(ipfs); + const ensAddress: string | undefined = ens; + + // Web3Api Project + const project = new Web3ApiProject({ + rootCacheDir: path.dirname(manifestFile), + web3apiManifestPath: manifestFile, + }); + await project.validate(); + const schemaComposer = new SchemaComposer({ + project, + ipfsProvider, + ethProvider, + ensAddress, + }); + + let result = false; + if (script) { + const codeGenerator = new CodeGenerator({ + project, + schemaComposer, + customScript: script, + outputDir: codegenDir, + }); - if (script === true) { - const customScriptMissingPathMessage = intlMsg.commands_codegen_error_optionMissingArgument( - { - option: "--script", - argument: `<${pathStr}>`, - } - ); - print.error(customScriptMissingPathMessage); - return false; - } + result = await codeGenerator.generate(); + } else { + const compiler = new Compiler({ + project, + outputDir: filesystem.path("build"), + schemaComposer, + }); - if (ipfs === true) { - const ipfsMissingMessage = intlMsg.commands_codegen_error_optionMissingArgument( - { - option: "--ipfs", - argument: `[<${nodeStr}>]`, - } - ); - print.error(ipfsMissingMessage); - return false; + result = await compiler.codegen(); } - if (ens === true) { - const ensAddressMissingMessage = intlMsg.commands_codegen_error_optionMissingArgument( - { - option: "--ens", - argument: `[<${addrStr}>]`, - } - ); - print.error(ensAddressMissingMessage); - return false; + if (result) { + console.log(`🔥 ${intlMsg.commands_codegen_success()} 🔥`); + process.exitCode = 0; + } else { + process.exitCode = 1; } - - return true; } diff --git a/packages/cli/src/commands/create.ts b/packages/cli/src/commands/create.ts index 652ac477aa..5524d89218 100644 --- a/packages/cli/src/commands/create.ts +++ b/packages/cli/src/commands/create.ts @@ -1,11 +1,10 @@ -import { generateProjectTemplate, fixParameters, intlMsg } from "../lib"; +import { Command, Program } from "./types"; +import { generateProjectTemplate, intlMsg } from "../lib"; -import chalk from "chalk"; -import { GluegunToolbox, GluegunPrint } from "gluegun"; +import { prompt, filesystem } from "gluegun"; +import { Argument } from "commander"; -const cmdStr = intlMsg.commands_create_options_command(); const nameStr = intlMsg.commands_create_options_projectName(); -const optionsStr = intlMsg.commands_options_options(); const langStr = intlMsg.commands_create_options_lang(); const langsStr = intlMsg.commands_create_options_langs(); const createProjStr = intlMsg.commands_create_options_createProject(); @@ -13,170 +12,132 @@ const createAppStr = intlMsg.commands_create_options_createApp(); const createPluginStr = intlMsg.commands_create_options_createPlugin(); const pathStr = intlMsg.commands_create_options_o_path(); -export const supportedLangs: { [key: string]: string[] } = { - api: ["assemblyscript", "interface"], - app: ["typescript-node", "typescript-react"], - plugin: ["typescript"], +const supportedLangs = { + api: ["assemblyscript", "interface"] as const, + app: ["typescript-node", "typescript-react"] as const, + plugin: ["typescript"] as const, }; -const HELP = ` -${chalk.bold("w3 create")} ${cmdStr} <${nameStr}> [${optionsStr}] - -${intlMsg.commands_create_options_commands()}: - ${chalk.bold("api")} <${langStr}> ${createProjStr} - ${langsStr}: ${supportedLangs.api.join(", ")} - ${chalk.bold("app")} <${langStr}> ${createAppStr} - ${langsStr}: ${supportedLangs.app.join(", ")} - ${chalk.bold("plugin")} <${langStr}> ${createPluginStr} - ${langsStr}: ${supportedLangs.plugin.join(", ")} - -Options: - -h, --help ${intlMsg.commands_create_options_h()} - -o, --output-dir <${pathStr}> ${intlMsg.commands_create_options_o()} -`; - -export default { - alias: ["c"], - description: intlMsg.commands_create_description(), - run: async (toolbox: GluegunToolbox): Promise => { - const { parameters, print, prompt, filesystem } = toolbox; - - // Options - let { help, outputDir } = parameters.options; - const { h, o } = parameters.options; - - help = help || h; - outputDir = outputDir || o; - - let type = ""; - let lang = ""; - let name = ""; - try { - const params = parameters; - [type, lang, name] = fixParameters( - { - options: params.options, - array: params.array, - }, - { - h, - help, - } - ); - // eslint-disable-next-line no-empty - } catch (e) {} - - if (help) { - print.info(HELP); - return; - } - - // Validate Params - const paramsValid = validateCreateParams( - print, - type, - lang, - name, - outputDir - ); - - if (!paramsValid) { - print.info(HELP); - process.exitCode = 1; - return; - } +export type ProjectType = keyof typeof supportedLangs; +export type SupportedLangs = typeof supportedLangs[ProjectType][number]; +type CreateCommandOptions = { + outputDir?: string; +}; - const projectDir = outputDir ? `${outputDir}/${name}` : name; +export const create: Command = { + setup: (program: Program) => { + const createCommand = program + .command("create") + .alias("c") + .description(intlMsg.commands_create_description()); + + createCommand + .command("api") + .description( + `${createProjStr} ${langsStr}: ${supportedLangs.api.join(", ")}` + ) + .addArgument( + new Argument("", langStr) + .choices(supportedLangs.api) + .argRequired() + ) + .addArgument(new Argument("", nameStr).argRequired()) + .option( + `-o, --output-dir <${pathStr}>`, + `${intlMsg.commands_create_options_o()}` + ) + .action(async (langStr, nameStr, options) => { + await run("api", langStr, nameStr, options); + }); - // check if project already exists - if (!filesystem.exists(projectDir)) { - print.newline(); - print.info(intlMsg.commands_create_settingUp()); - } else { - const directoryExistsMessage = intlMsg.commands_create_directoryExists({ - dir: projectDir, + createCommand + .command("app") + .description( + `${createAppStr} ${langsStr}: ${supportedLangs.app.join(", ")}` + ) + .addArgument( + new Argument("", langStr) + .choices(supportedLangs.app) + .argRequired() + ) + .option( + `-o, --output-dir <${pathStr}>`, + `${intlMsg.commands_create_options_o()}` + ) + .action(async (langStr, nameStr, options) => { + await run("app", langStr, nameStr, options); }); - print.info(directoryExistsMessage); - const overwrite = await prompt.confirm( - intlMsg.commands_create_overwritePrompt() - ); - if (overwrite) { - const overwritingMessage = intlMsg.commands_create_overwriting({ - dir: projectDir, - }); - print.info(overwritingMessage); - filesystem.remove(projectDir); - } else { - process.exit(8); - } - } - generateProjectTemplate(type, lang, projectDir, filesystem) - .then(() => { - print.newline(); - let readyMessage; - if (type === "api") { - readyMessage = intlMsg.commands_create_readyProtocol(); - } else if (type === "app") { - readyMessage = intlMsg.commands_create_readyApp(); - } else if (type === "plugin") { - readyMessage = intlMsg.commands_create_readyPlugin(); - } - print.info(`🔥 ${readyMessage} 🔥`); - }) - .catch((err) => { - const commandFailError = intlMsg.commands_create_error_commandFail({ - error: err.command, - }); - print.error(commandFailError); + createCommand + .command(`plugin <${langStr}>`) + .description( + `${createPluginStr} ${langsStr}: ${supportedLangs.plugin.join(", ")}` + ) + .addArgument( + new Argument("", langStr) + .choices(supportedLangs.plugin) + .argRequired() + ) + .option( + `-o, --output-dir <${pathStr}>`, + `${intlMsg.commands_create_options_o()}` + ) + .action(async (langStr, nameStr, options) => { + await run("plugin", langStr, nameStr, options); }); }, }; -function validateCreateParams( - print: GluegunPrint, - type: unknown, - lang: unknown, - name: unknown, - outputDir: unknown -): boolean { - if (!type || typeof type !== "string") { - print.error(intlMsg.commands_create_error_noCommand()); - return false; - } - - if (!lang || typeof lang !== "string") { - print.error(intlMsg.commands_create_error_noLang()); - return false; - } - - if (!name || typeof name !== "string") { - print.error(intlMsg.commands_create_error_noName()); - return false; - } - - if (!supportedLangs[type]) { - const unrecognizedCommand = intlMsg.commands_create_error_unrecognizedCommand(); - print.error(`${unrecognizedCommand} "${type}"`); - return false; - } - - if (supportedLangs[type].indexOf(lang) === -1) { - const unrecognizedLanguage = intlMsg.commands_create_error_unrecognizedLanguage(); - print.error(`${unrecognizedLanguage} "${lang}"`); - return false; - } - - if (outputDir === true) { - const outputDirMissingPathMessage = intlMsg.commands_create_error_outputDirMissingPath( - { - option: "--output-dir", - argument: `<${pathStr}>`, - } +async function run( + command: ProjectType, + lang: SupportedLangs, + name: string, + options: CreateCommandOptions +) { + const { outputDir } = options; + + const projectDir = outputDir ? `${outputDir}/${name}` : name; + + // check if project already exists + if (!filesystem.exists(projectDir)) { + console.log(); + console.info(intlMsg.commands_create_settingUp()); + } else { + const directoryExistsMessage = intlMsg.commands_create_directoryExists({ + dir: projectDir, + }); + console.info(directoryExistsMessage); + const overwrite = await prompt.confirm( + intlMsg.commands_create_overwritePrompt() ); - print.error(outputDirMissingPathMessage); - return false; + if (overwrite) { + const overwritingMessage = intlMsg.commands_create_overwriting({ + dir: projectDir, + }); + console.info(overwritingMessage); + filesystem.remove(projectDir); + } else { + process.exit(8); + } } - return true; + generateProjectTemplate(command, lang, projectDir, filesystem) + .then(() => { + console.log(); + let readyMessage; + if (command === "api") { + readyMessage = intlMsg.commands_create_readyProtocol(); + } else if (command === "app") { + readyMessage = intlMsg.commands_create_readyApp(); + } else if (command === "plugin") { + readyMessage = intlMsg.commands_create_readyPlugin(); + } + console.info(`🔥 ${readyMessage} 🔥`); + }) + .catch((err) => { + const commandFailError = intlMsg.commands_create_error_commandFail({ + error: err.command, + }); + console.error(commandFailError); + }); } diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 4b8bac59a2..35b813c9d9 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -3,175 +3,132 @@ import { intlMsg, Web3ApiProject, defaultWeb3ApiManifest, - resolvePathIfExists, DeployPackage, } from "../lib"; +import { parseWasmManifestFileOption } from "../lib/parsers"; import { DeployerHandler } from "../lib/deploy/deployer"; +import { Command, Program } from "./types"; -import chalk from "chalk"; import fs from "fs"; import nodePath from "path"; -import { GluegunToolbox, GluegunPrint } from "gluegun"; +import { print } from "gluegun"; import { Uri, DeployManifest } from "@web3api/core-js"; import { validate } from "jsonschema"; const defaultManifestStr = defaultWeb3ApiManifest.join(" | "); -const optionsStr = intlMsg.commands_deploy_options_options(); const pathStr = intlMsg.commands_deploy_options_o_path(); -const HELP = ` -${chalk.bold("w3 deploy")} [${optionsStr}] - -${optionsStr[0].toUpperCase() + optionsStr.slice(1)}: - -h, --help ${intlMsg.commands_deploy_options_h()} - -m, --manifest-file <${pathStr}> ${intlMsg.commands_deploy_options_m({ - default: defaultManifestStr, -})} - -v, --verbose ${intlMsg.commands_deploy_options_v()}`; - -export default { - alias: ["b"], - description: intlMsg.commands_deploy_description(), - run: async (toolbox: GluegunToolbox): Promise => { - const { filesystem, parameters, print } = toolbox; - - // Options - const { h, m, v } = parameters.options; - let { help, manifestFile, verbose } = parameters.options; - - help = help || h; - verbose = verbose || v; - manifestFile = manifestFile || m; - - // Validate Params - const paramsValid = validateDeployParams(print, { - manifestFile, - }); - - if (help || !paramsValid) { - print.info(HELP); - if (!paramsValid) { - process.exitCode = 1; - } - return; - } +type DeployCommandOptions = { + manifestFile: string; + verbose?: boolean; +}; - // Resolve manifest & output directory - const manifestPaths = manifestFile - ? [manifestFile as string] - : defaultWeb3ApiManifest; - manifestFile = resolvePathIfExists(filesystem, manifestPaths); - - if (!manifestFile) { - print.error( - intlMsg.commands_build_error_manifestNotFound({ - paths: manifestPaths.join(", "), - }) - ); - return; - } +export const deploy: Command = { + setup: (program: Program) => { + program + .command("deploy") + .alias("d") + .description(intlMsg.commands_deploy_description()) + .option( + `-m, --manifest-file <${pathStr}>`, + `${intlMsg.commands_deploy_options_m({ + default: defaultManifestStr, + })}` + ) + .option(`-v, --verbose`, `${intlMsg.commands_deploy_options_v()}`) + .action(async (options) => { + await run({ + ...options, + manifestFile: parseWasmManifestFileOption( + options.manifestFile, + undefined + ), + }); + }); + }, +}; - const project = new Web3ApiProject({ - rootCacheDir: nodePath.dirname(manifestFile), - web3apiManifestPath: manifestFile, - quiet: verbose ? false : true, - }); +async function run(options: DeployCommandOptions): Promise { + const { manifestFile, verbose } = options; - await project.validate(); + const project = new Web3ApiProject({ + rootCacheDir: nodePath.dirname(manifestFile), + web3apiManifestPath: manifestFile, + quiet: verbose ? false : true, + }); - const deployManifest = await project.getDeployManifest(); + await project.validate(); - if (!deployManifest) { - throw new Error("No deploy manifest found."); - } + const deployManifest = await project.getDeployManifest(); - const packageNames = [ - ...new Set(Object.values(deployManifest.stages).map((d) => d.package)), - ]; + if (!deployManifest) { + throw new Error("No deploy manifest found."); + } - sanitizePackages(packageNames); + const packageNames = [ + ...new Set(Object.values(deployManifest.stages).map((d) => d.package)), + ]; - await project.cacheDeploymentPackages(packageNames); + sanitizePackages(packageNames); - const packageMap: Record = {}; - const stageToPackageMap: Record = {}; + await project.cacheDeploymentPackages(packageNames); - for await (const packageName of packageNames) { - packageMap[packageName] = await project.getDeploymentPackage(packageName); - } + const packageMap: Record = {}; + const stageToPackageMap: Record = {}; - Object.entries(deployManifest.stages).forEach(([stageName, stageValue]) => { - stageToPackageMap[stageName] = packageMap[stageValue.package]; - }); + for await (const packageName of packageNames) { + packageMap[packageName] = await project.getDeploymentPackage(packageName); + } - validateManifestWithExts(deployManifest, stageToPackageMap); + Object.entries(deployManifest.stages).forEach(([stageName, stageValue]) => { + stageToPackageMap[stageName] = packageMap[stageValue.package]; + }); - const handlers: Record = {}; - const roots: { handler: DeployerHandler; uri: Uri }[] = []; + validateManifestWithExts(deployManifest, stageToPackageMap); - // Create all handlers - Object.entries(deployManifest.stages).forEach(([stageName, stageValue]) => { - const publisher = stageToPackageMap[stageName].deployer; - const handler = new DeployerHandler( - stageName, - publisher, - stageValue.config, - print - ); + const handlers: Record = {}; + const roots: { handler: DeployerHandler; uri: Uri }[] = []; + + // Create all handlers + Object.entries(deployManifest.stages).forEach(([stageName, stageValue]) => { + const publisher = stageToPackageMap[stageName].deployer; + const handler = new DeployerHandler( + stageName, + publisher, + stageValue.config, + print + ); - handlers[stageName] = handler; - }); - - // Establish dependency chains - Object.entries(deployManifest.stages).forEach(([key, value]) => { - const thisHandler = handlers[key]; - - if (value.depends_on) { - // Depends on another stage - handlers[value.depends_on].addNext(thisHandler); - } else if (value.uri) { - // It is a root node - roots.push({ uri: new Uri(value.uri), handler: thisHandler }); - } else { - throw new Error( - `Stage '${key}' needs either previous (depends_on) stage or URI` - ); - } - }); - - // Execute roots - - for await (const root of roots) { - print.info(`\nExecuting deployment chain: \n`); - root.handler.getDependencyTree().printTree(); - print.info(""); - await root.handler.handle(root.uri); + handlers[stageName] = handler; + }); + + // Establish dependency chains + Object.entries(deployManifest.stages).forEach(([key, value]) => { + const thisHandler = handlers[key]; + + if (value.depends_on) { + // Depends on another stage + handlers[value.depends_on].addNext(thisHandler); + } else if (value.uri) { + // It is a root node + roots.push({ uri: new Uri(value.uri), handler: thisHandler }); + } else { + throw new Error( + `Stage '${key}' needs either previous (depends_on) stage or URI` + ); } + }); - process.exitCode = 0; - }, -}; + // Execute roots -function validateDeployParams( - print: GluegunPrint, - params: { - manifestFile: unknown; - } -): boolean { - const { manifestFile } = params; - - if (manifestFile === true) { - const manifestPathMissingMessage = intlMsg.commands_build_error_manifestPathMissing( - { - option: "--manifest-file", - argument: `<${pathStr}>`, - } - ); - print.error(manifestPathMissingMessage); - return false; + for await (const root of roots) { + print.info(`\nExecuting deployment chain: \n`); + root.handler.getDependencyTree().printTree(); + print.info(""); + await root.handler.handle(root.uri); } - return true; + return; } function sanitizePackages(packages: string[]) { diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts new file mode 100644 index 0000000000..5c76003da2 --- /dev/null +++ b/packages/cli/src/commands/index.ts @@ -0,0 +1,8 @@ +export * from "./app"; +export * from "./build"; +export * from "./codegen"; +export * from "./create"; +export * from "./deploy"; +export * from "./plugin"; +export * from "./query"; +export * from "./test-env"; diff --git a/packages/cli/src/commands/plugin.ts b/packages/cli/src/commands/plugin.ts index e91e50f3d1..afce0295f8 100644 --- a/packages/cli/src/commands/plugin.ts +++ b/packages/cli/src/commands/plugin.ts @@ -1,246 +1,141 @@ -/* eslint-disable prefer-const */ +import { Command, Program } from "./types"; import { CodeGenerator, PluginProject, SchemaComposer, - fixParameters, - resolvePathIfExists, defaultPluginManifest, outputManifest, intlMsg, getTestEnvProviders, } from "../lib"; +import { + parsePluginCodegenDirOption, + parsePluginManifestFileOption, + parsePluginPublishDirOption, +} from "../lib/parsers"; import { ComposerFilter } from "@web3api/schema-compose"; import { writeFileSync } from "@web3api/os-js"; -import { GluegunPrint, GluegunToolbox, print } from "gluegun"; -import chalk from "chalk"; import path from "path"; import fs from "fs"; -const commands = ["codegen"]; const defaultPublishDir = "./build"; const defaultCodegenDir = "./src/w3"; -const cmdStr = intlMsg.commands_plugin_options_command(); -const optionsStr = intlMsg.commands_options_options(); -const codegenStr = intlMsg.commands_plugin_commands_codegen(); const pathStr = intlMsg.commands_plugin_options_path(); const defaultManifestStr = defaultPluginManifest.join(" | "); const nodeStr = intlMsg.commands_plugin_options_i_node(); const addrStr = intlMsg.commands_plugin_options_e_address(); -const HELP = ` -${chalk.bold("w3 plugin")} ${cmdStr} [${optionsStr}] - -Commands: - ${chalk.bold("codegen")} ${codegenStr} - -Options: - -h, --help ${intlMsg.commands_plugin_options_h()} - -m, --manifest-file <${pathStr}> ${intlMsg.commands_plugin_options_m({ - default: defaultManifestStr, -})} - -p, --publish-dir <${pathStr}> ${intlMsg.commands_plugin_options_publish({ - default: defaultPublishDir, -})} - -c, --codegen-dir <${pathStr}> ${intlMsg.commands_plugin_options_codegen({ - default: defaultCodegenDir, -})} - -i, --ipfs [<${nodeStr}>] ${intlMsg.commands_plugin_options_i()} - -e, --ens [<${addrStr}>] ${intlMsg.commands_plugin_options_e()} -`; - -export default { - alias: ["p"], - description: intlMsg.commands_plugin_description(), - run: async (toolbox: GluegunToolbox): Promise => { - const { filesystem, parameters } = toolbox; - - // Options - let { - help, - manifestFile, - publishDir, - codegenDir, - ipfs, - ens, - } = parameters.options; - const { h, m, p, c, i, e } = parameters.options; - - help = help || h; - manifestFile = manifestFile || m; - publishDir = publishDir || p; - codegenDir = codegenDir || c; - ipfs = ipfs || i; - ens = ens || e; - - // Command - let command: string | undefined; - try { - const params = parameters; - [command] = fixParameters( - { - options: params.options, - array: params.array, - }, - { - h, - help, - } - ); - // eslint-disable-next-line no-empty - } catch (e) {} - - if (help) { - print.info(HELP); - return; - } - - // Validate Params - const paramsValid = validatePluginParams( - print, - command, - publishDir, - (dir) => (publishDir = dir), - codegenDir, - (dir) => (codegenDir = dir), - ipfs, - ens - ); - - if (!paramsValid) { - print.info(HELP); - process.exitCode = 1; - return; - } - - const { ipfsProvider, ethProvider } = await getTestEnvProviders(ipfs); - const ensAddress: string | undefined = ens; - - manifestFile = resolvePathIfExists( - filesystem, - manifestFile ? [manifestFile] : defaultPluginManifest - ); - publishDir = publishDir && filesystem.resolve(publishDir); - codegenDir = codegenDir && filesystem.resolve(codegenDir); - - // Plugin project - const project = new PluginProject({ - rootCacheDir: path.dirname(manifestFile), - pluginManifestPath: manifestFile, - }); - await project.validate(); - const manifest = await project.getManifest(); - - const schemaComposer = new SchemaComposer({ - project, - ipfsProvider, - ethProvider, - ensAddress, - }); - - let result = false; - - const codeGenerator = new CodeGenerator({ - project, - schemaComposer, - outputDir: codegenDir, - }); - - result = await codeGenerator.generate(); - - if (result) { - process.exitCode = 0; - } else { - process.exitCode = 1; - } - - // Output the built schema & manifest - const schemas = await schemaComposer.getComposedSchemas( - ComposerFilter.Schema - ); - const publishSchemaPath = path.join(publishDir, "schema.graphql"); - const publishManifestPath = path.join(publishDir, "web3api.plugin.json"); - - if (!fs.existsSync(publishDir)) { - fs.mkdirSync(publishDir); - } +type PluginCommandOptions = { + manifestFile: string; + publishDir: string; + codegenDir: string; + ipfs?: string; + ens?: string; +}; - writeFileSync(publishSchemaPath, schemas.combined.schema); - await outputManifest(manifest, publishManifestPath); +export const plugin: Command = { + setup: (program: Program) => { + const pluginCommand = program + .command("plugin") + .alias("p") + .description(intlMsg.commands_plugin_description()); + + pluginCommand + .command("codegen") + .option( + `-m, --manifest-file <${pathStr}>`, + `${intlMsg.commands_plugin_options_m({ + default: defaultManifestStr, + })}` + ) + .option( + `-p, --publish-dir <${pathStr}>`, + `${intlMsg.commands_plugin_options_publish({ + default: defaultPublishDir, + })}` + ) + .option( + `-c, --codegen-dir <${pathStr}>`, + `${intlMsg.commands_plugin_options_codegen({ + default: defaultCodegenDir, + })}` + ) + .option( + `-i, --ipfs [<${nodeStr}>]`, + `${intlMsg.commands_plugin_options_i()}` + ) + .option( + `-e, --ens [<${addrStr}>]`, + `${intlMsg.commands_plugin_options_e()}` + ) + .action(async (options) => { + await run({ + ...options, + manifestFile: parsePluginManifestFileOption( + options.manifestFile, + undefined + ), + publishDir: parsePluginPublishDirOption( + options.publishDir, + undefined + ), + codegenDir: parsePluginCodegenDirOption( + options.codegenDir, + undefined + ), + }); + }); }, }; - -function validatePluginParams( - print: GluegunPrint, - command: unknown, - publishDir: unknown, - setPublishDir: (dir: string) => void, - codegenDir: unknown, - setCodegenDir: (dir: string) => void, - ipfs: unknown, - ens: unknown -): boolean { - if (!command) { - print.error(intlMsg.commands_plugin_error_noCommand()); - return false; +async function run(options: PluginCommandOptions) { + const { ipfs, ens, manifestFile, codegenDir, publishDir } = options; + + const { ipfsProvider, ethProvider } = await getTestEnvProviders(ipfs); + const ensAddress: string | undefined = ens; + + // Plugin project + const project = new PluginProject({ + rootCacheDir: path.dirname(manifestFile), + pluginManifestPath: manifestFile, + }); + await project.validate(); + const manifest = await project.getManifest(); + + const schemaComposer = new SchemaComposer({ + project, + ipfsProvider, + ethProvider, + ensAddress, + }); + + let result = false; + + const codeGenerator = new CodeGenerator({ + project, + schemaComposer, + outputDir: codegenDir, + }); + + result = await codeGenerator.generate(); + + if (result) { + process.exitCode = 0; + } else { + process.exitCode = 1; } - if (!command || typeof command !== "string") { - print.error(intlMsg.commands_plugin_error_noCommand()); - return false; - } else if (commands.indexOf(command) === -1) { - print.error(intlMsg.commands_plugin_error_unknownCommand({ command })); - return false; - } - - if (publishDir === true) { - const publishDirMessage = intlMsg.commands_plugin_error_optionMissingArgument( - { - option: "--publish-dir", - argument: `<${pathStr}>`, - } - ); - print.error(publishDirMessage); - return false; - } else if (!publishDir) { - setPublishDir(defaultPublishDir); - } - - if (codegenDir === true) { - const codegenDirMessage = intlMsg.commands_plugin_error_optionMissingArgument( - { - option: "--codegen-dir", - argument: `<${pathStr}>`, - } - ); - print.error(codegenDirMessage); - return false; - } else if (!codegenDir) { - setCodegenDir(defaultCodegenDir); - } - - if (ipfs === true) { - const ipfsMissingMessage = intlMsg.commands_plugin_error_optionMissingArgument( - { - option: "--ipfs", - argument: `[<${nodeStr}>]`, - } - ); - print.error(ipfsMissingMessage); - return false; - } + // Output the built schema & manifest + const schemas = await schemaComposer.getComposedSchemas( + ComposerFilter.Schema + ); + const publishSchemaPath = path.join(publishDir, "schema.graphql"); + const publishManifestPath = path.join(publishDir, "web3api.plugin.json"); - if (ens === true) { - const ensAddressMissingMessage = intlMsg.commands_plugin_error_optionMissingArgument( - { - option: "--ens", - argument: `[<${addrStr}>]`, - } - ); - print.error(ensAddressMissingMessage); - return false; + if (!fs.existsSync(publishDir)) { + fs.mkdirSync(publishDir); } - return true; + writeFileSync(publishSchemaPath, schemas.combined.schema); + await outputManifest(manifest, publishManifestPath); } diff --git a/packages/cli/src/commands/query.ts b/packages/cli/src/commands/query.ts index 79fbeec714..dabd8175e3 100644 --- a/packages/cli/src/commands/query.ts +++ b/packages/cli/src/commands/query.ts @@ -1,283 +1,171 @@ +import { Command, Program } from "./types"; +import { intlMsg } from "../lib"; import { - getTestEnvClientConfig, - importTypescriptModule, - validateClientConfig, - fixParameters, - intlMsg, -} from "../lib"; + parseClientConfigOption, + parseRecipeOutputFilePathOption, +} from "../lib/parsers"; -import { - executeMaybeAsyncFunction, - Web3ApiClient, - Web3ApiClientConfig, -} from "@web3api/client-js"; -import chalk from "chalk"; -import { GluegunToolbox } from "gluegun"; +import { Web3ApiClient, Web3ApiClientConfig } from "@web3api/client-js"; import gql from "graphql-tag"; import path from "path"; import yaml from "js-yaml"; import fs from "fs"; -const optionsString = intlMsg.commands_build_options_options(); -const scriptStr = intlMsg.commands_create_options_recipeScript(); -const configPathStr = intlMsg.commands_query_options_configPath(); -const outputFileStr = intlMsg.commands_query_options_outputFile(); - -const HELP = ` -${chalk.bold("w3 query")} [${optionsString}] ${chalk.bold(`<${scriptStr}>`)} - -${optionsString[0].toUpperCase() + optionsString.slice(1)}: - -h, --help ${intlMsg.commands_build_options_h()} - -c, --client-config <${configPathStr}> ${intlMsg.commands_query_options_config()} - -o, --output-file ${intlMsg.commands_query_options_outputFile()} - -q, --quiet ${intlMsg.commands_query_options_quiet()} -`; - -export default { - alias: ["q"], - description: intlMsg.commands_query_description(), - run: async (toolbox: GluegunToolbox): Promise => { - const { filesystem, parameters, print } = toolbox; - - // Options - let { help, clientConfig, outputFile, quiet } = parameters.options; - const { h, c, o, q } = parameters.options; - - help = help || h; - clientConfig = clientConfig || c; - outputFile = outputFile || o; - quiet = quiet || q; - - let recipePath; - try { - const params = toolbox.parameters; - [recipePath] = fixParameters( - { - options: params.options, - array: params.array, - }, - { - h, - help, - } - ); - } catch (e) { - recipePath = null; - print.error(e.message); - process.exitCode = 1; - return; - } - - if (help) { - print.info(HELP); - return; - } +type QueryCommandOptions = { + clientConfig: Partial; + outputFile?: string; + quiet?: boolean; +}; - if (!recipePath) { - const scriptMissingMessage = intlMsg.commands_query_error_missingScript({ - script: `<${scriptStr}>`, +export const query: Command = { + setup: (program: Program) => { + program + .command("query") + .alias("q") + .description(intlMsg.commands_query_description()) + .argument("", intlMsg.commands_query_options_recipeScript()) + .option( + `-c, --client-config <${intlMsg.commands_query_options_configPath()}> `, + `${intlMsg.commands_query_options_config()}` + ) + .option( + `-o, --output-file <${intlMsg.commands_query_options_outputFilePath()}>`, + `${intlMsg.commands_query_options_outputFile()}` + ) + .option(`-q, --quiet`, `${intlMsg.commands_query_options_quiet()}`) + .action(async (recipe: string, options) => { + await run(recipe, { + ...options, + clientConfig: await parseClientConfigOption( + options.clientConfig, + undefined + ), + outputFile: options.outputFile + ? parseRecipeOutputFilePathOption(options.outputFile, undefined) + : undefined, + }); }); - print.error(scriptMissingMessage); - print.info(HELP); - return; - } - - if (outputFile === true) { - const outputFileMissingMessage = intlMsg.commands_query_error_outputFileMissing( - { - option: "--output-file", - argument: `<${outputFileStr}>`, - } - ); - print.error(outputFileMissingMessage); - print.info(HELP); - return; - } - - if (clientConfig === true) { - const configMissingPathMessage = intlMsg.commands_query_error_clientConfigMissingPath( - { - option: "--client-config", - argument: `<${configPathStr}>`, - } - ); - print.error(configMissingPathMessage); - print.info(HELP); - return; - } - - let finalClientConfig: Partial; + }, +}; - try { - finalClientConfig = await getTestEnvClientConfig(); - } catch (e) { - print.error(intlMsg.commands_query_error_noTestEnvFound()); - process.exitCode = 1; - return; +async function run(recipePath: string, options: QueryCommandOptions) { + const { clientConfig, outputFile, quiet } = options; + const client = new Web3ApiClient(clientConfig); + + function getParser(path: string) { + return path.endsWith(".yaml") || path.endsWith(".yml") + ? yaml.load + : JSON.parse; + } + + const recipe = getParser(recipePath)(fs.readFileSync(recipePath).toString()); + const dir = path.dirname(recipePath); + let uri = ""; + const recipeOutput = []; + + let constants: Record = {}; + for (const task of recipe) { + if (task.api) { + uri = task.api; + recipeOutput.push({ api: task.api }); } - if (clientConfig) { - let configModule; - if (clientConfig.endsWith(".js")) { - configModule = await import(filesystem.resolve(clientConfig)); - } else if (clientConfig.endsWith(".ts")) { - configModule = await importTypescriptModule( - filesystem.resolve(clientConfig) - ); - } else { - const configsModuleMissingExportMessage = intlMsg.commands_query_error_clientConfigInvalidFileExt( - { module: clientConfig } - ); - print.error(configsModuleMissingExportMessage); - process.exitCode = 1; - return; - } - - if (!configModule || !configModule.getClientConfig) { - const configsModuleMissingExportMessage = intlMsg.commands_query_error_clientConfigModuleMissingExport( - { module: configModule } - ); - print.error(configsModuleMissingExportMessage); - process.exitCode = 1; - return; - } - - finalClientConfig = await executeMaybeAsyncFunction( - configModule.getClientConfig, - finalClientConfig + if (task.constants) { + constants = getParser(task.constants)( + fs.readFileSync(path.join(dir, task.constants)).toString() ); - - try { - validateClientConfig(finalClientConfig); - } catch (e) { - print.error(e.message); - process.exitCode = 1; - return; - } - } - - const client = new Web3ApiClient(finalClientConfig); - - function getParser(path: string) { - return path.endsWith(".yaml") || path.endsWith(".yml") - ? yaml.load - : JSON.parse; + recipeOutput.push({ constants: task.constants }); } - const recipe = getParser(recipePath)(filesystem.read(recipePath) as string); - const dir = path.dirname(recipePath); - let uri = ""; - const recipeOutput = []; + if (task.query) { + const query = fs.readFileSync(path.join(dir, task.query)).toString(); - let constants: Record = {}; - for (const task of recipe) { - if (task.api) { - uri = task.api; - recipeOutput.push({ - api: task.api, + if (!query) { + const readFailMessage = intlMsg.commands_query_error_readFail({ + query: query ?? "undefined", }); + console.error(readFailMessage); + process.exit(1); } - if (task.constants) { - constants = getParser(task.constants)( - filesystem.read(path.join(dir, task.constants)) as string - ); - recipeOutput.push({ - constants: task.constants, - }); - } + let variables: Record = {}; - if (task.query) { - const query = filesystem.read(path.join(dir, task.query)); + if (task.variables) { + const resolveObjectConstants = ( + constants: Record + ): Record => { + const output: Record = {}; - if (!query) { - const readFailMessage = intlMsg.commands_query_error_readFail({ - query: query ?? "undefined", + Object.keys(constants).forEach((key: string) => { + output[key] = resolveConstant(constants[key]); }); - throw Error(readFailMessage); - } - - let variables: Record = {}; - - if (task.variables) { - const resolveObjectConstants = ( - constants: Record - ): Record => { - const output: Record = {}; - - Object.keys(constants).forEach((key: string) => { - output[key] = resolveConstant(constants[key]); - }); - return output; - }; + return output; + }; - const resolveArrayConstants = (arr: unknown[]): unknown[] => { - return arr.map((item) => { - return resolveConstant(item); - }); - }; + const resolveArrayConstants = (arr: unknown[]): unknown[] => { + return arr.map((item) => { + return resolveConstant(item); + }); + }; + + const resolveConstant = (constant: unknown): unknown => { + if (typeof constant === "string" && constant[0] === "$") { + return constants[constant.replace("$", "")]; + } else if (Array.isArray(constant)) { + return resolveArrayConstants(constant); + } else if (typeof constant === "object") { + return resolveObjectConstants(constant as Record); + } else { + return constant; + } + }; - const resolveConstant = (constant: unknown): unknown => { - if (typeof constant === "string" && constant[0] === "$") { - return constants[constant.replace("$", "")]; - } else if (Array.isArray(constant)) { - return resolveArrayConstants(constant); - } else if (typeof constant === "object") { - return resolveObjectConstants( - constant as Record - ); - } else { - return constant; - } - }; + variables = resolveObjectConstants(task.variables); + } - variables = resolveObjectConstants(task.variables); - } + if (!uri) { + throw Error(intlMsg.commands_query_error_noApi()); + } - if (!uri) { - throw Error(intlMsg.commands_query_error_noApi()); - } + if (!quiet) { + console.log("-----------------------------------"); + console.log(query); + console.log(JSON.stringify(variables, null, 2)); + console.log("-----------------------------------"); + } - if (!quiet) { - print.warning("-----------------------------------"); - print.fancy(query); - print.fancy(JSON.stringify(variables, null, 2)); - print.warning("-----------------------------------"); - } + const { data, errors } = await client.query({ + uri, + query: gql(query), + variables, + }); - const { data, errors } = await client.query({ - uri, - query: gql(query), - variables, + if (outputFile) { + recipeOutput.push({ + query: task.query, + variables: task.variables, + output: { + data, + errors, + }, }); + } - if (outputFile) { - recipeOutput.push({ - query: task.query, - variables: task.variables, - output: { - data, - errors, - }, - }); - } - - if (!quiet && data && data !== {}) { - print.success("-----------------------------------"); - print.fancy(JSON.stringify(data, null, 2)); - print.success("-----------------------------------"); - } + if (!quiet && data && data !== {}) { + console.log("-----------------------------------"); + console.log(JSON.stringify(data, null, 2)); + console.log("-----------------------------------"); + } - if (!quiet && errors) { - for (const error of errors) { - print.error("-----------------------------------"); - print.fancy(error.message); - print.fancy(error.stack || ""); - print.error("-----------------------------------"); - } - process.exitCode = 1; + if (!quiet && errors) { + for (const error of errors) { + console.log("-----------------------------------"); + console.log(error.message); + console.log(error.stack || ""); + console.log("-----------------------------------"); } + process.exitCode = 1; } } @@ -296,5 +184,5 @@ export default { throw new Error(`Unsupported outputFile extention: ${outputFileExt}`); } } - }, -}; + } +} diff --git a/packages/cli/src/commands/test-env.ts b/packages/cli/src/commands/test-env.ts index 09f384d99e..c76d1b883b 100644 --- a/packages/cli/src/commands/test-env.ts +++ b/packages/cli/src/commands/test-env.ts @@ -1,94 +1,68 @@ +import { Command, Program } from "./types"; import { - startupTestEnv, - shutdownTestEnv, - withSpinner, intlMsg, isDockerInstalled, getDockerFileLock, + startupTestEnv, + shutdownTestEnv, } from "../lib"; -import { GluegunToolbox, print } from "gluegun"; -import chalk from "chalk"; - -const optionsString = intlMsg.commands_build_options_options(); - -const HELP = ` -${chalk.bold("w3 test-env")} ${intlMsg.commands_testEnv_options_command()} - -Commands: - ${chalk.bold("up")} ${intlMsg.commands_testEnv_options_start()} - ${chalk.bold("down")} ${intlMsg.commands_testEnv_options_stop()} - -${optionsString[0].toUpperCase() + optionsString.slice(1)}: - -h, --help ${intlMsg.commands_build_options_h()} -`; - -export default { - alias: ["t"], - description: intlMsg.commands_testEnv_description(), - run: async (toolbox: GluegunToolbox): Promise => { - const { parameters } = toolbox; - const { h } = parameters.options; - let { help } = parameters.options; +export const testEnv: Command = { + setup: (program: Program) => { + const testEnvCommand = program + .command("test-env") + .alias("t") + .description(intlMsg.commands_testEnv_description()); - help = help || h; + testEnvCommand + .command("up") + .description(intlMsg.commands_testEnv_options_start()) + .action(async () => { + await run("start"); + }); - if (help) { - print.info(HELP); - return; - } + testEnvCommand + .command("down") + .description(intlMsg.commands_testEnv_options_stop()) + .action(async () => { + await run("stop"); + }); + }, +}; - // Command - const command = parameters.first; +async function run(startStop: "start" | "stop") { + if (!isDockerInstalled()) { + throw new Error(intlMsg.lib_docker_noInstall()); + } - if (!command) { - print.error(intlMsg.commands_testEnv_error_noCommand()); - print.info(HELP); - return; - } + const dockerLock = getDockerFileLock(); + await dockerLock.request(); - if (command !== "up" && command !== "down") { - const unrecognizedCommandMessage = intlMsg.commands_testEnv_error_unrecognizedCommand( - { - command: command, - } - ); - print.error(unrecognizedCommandMessage); - print.info(HELP); - return; - } + if (startStop === "start") { + // TODO: create more robust logger + console.log(intlMsg.commands_testEnv_startup_text()); - if (!isDockerInstalled()) { - print.error(intlMsg.lib_docker_noInstall()); - return; + try { + await startupTestEnv(true); + await dockerLock.release(); + } catch (e) { + console.error(intlMsg.commands_testEnv_startup_error()); + throw e; } + } else if (startStop === "stop") { + // TODO: create more robust logger + console.log(intlMsg.commands_testEnv_shutdown_text()); - const dockerLock = getDockerFileLock(); - await dockerLock.request(); - - if (command === "up") { - return await withSpinner( - intlMsg.commands_testEnv_startup_text(), - intlMsg.commands_testEnv_startup_error(), - intlMsg.commands_testEnv_startup_warning(), - async (_spinner) => { - await startupTestEnv(true); - await dockerLock.release(); - } - ); - } else if (command === "down") { - return await withSpinner( - intlMsg.commands_testEnv_shutdown_text(), - intlMsg.commands_testEnv_shutdown_error(), - intlMsg.commands_testEnv_shutdown_warning(), - async (_spinner) => { - await shutdownTestEnv(true); - await dockerLock.release(); - } - ); - } else { + try { + // TODO: this should be configurable & stream the docker ouput to console for added info + await shutdownTestEnv(true); await dockerLock.release(); - throw Error(intlMsg.commands_testEnv_error_never()); + } catch (e) { + console.error(intlMsg.commands_testEnv_shutdown_error()); + throw e; } - }, -}; + } else { + await dockerLock.release(); + throw Error(intlMsg.commands_testEnv_error_never()); + } +} diff --git a/packages/cli/src/commands/types.ts b/packages/cli/src/commands/types.ts new file mode 100644 index 0000000000..4359d1afbc --- /dev/null +++ b/packages/cli/src/commands/types.ts @@ -0,0 +1,7 @@ +import { Command as Program } from "commander"; + +export { Program }; + +export interface Command { + setup: (program: Program) => void; +} diff --git a/packages/cli/src/commands/w3.ts b/packages/cli/src/commands/w3.ts deleted file mode 100644 index 15d199e92b..0000000000 --- a/packages/cli/src/commands/w3.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { intlMsg } from "../lib"; - -import { GluegunToolbox } from "gluegun"; - -export default { - alias: [], - description: "🔥 Web3API CLI 🔥", - run: async (toolbox: GluegunToolbox): Promise => { - const { print, parameters } = toolbox; - if (parameters.first !== undefined) { - const errorMessage = intlMsg.commands_w3_error_notACommand(); - print.error(`w3 ${parameters.first} ${errorMessage}`); - } else { - print.success( - intlMsg.commands_w3_helpPrompt({ - command: `${print.colors.blue("w3 help")}`, - }) - ); - } - }, -}; diff --git a/packages/cli/src/lib/manifest/app/load.ts b/packages/cli/src/lib/manifest/app/load.ts index 69e33a50d4..5158774198 100644 --- a/packages/cli/src/lib/manifest/app/load.ts +++ b/packages/cli/src/lib/manifest/app/load.ts @@ -11,7 +11,6 @@ export async function loadAppManifest( ): Promise { const run = (): Promise => { const manifest = fs.readFileSync(manifestPath, "utf-8"); - if (!manifest) { const noLoadMessage = intlMsg.lib_helpers_manifest_unableToLoad({ path: `${manifestPath}`, diff --git a/packages/cli/src/lib/parsers/app.ts b/packages/cli/src/lib/parsers/app.ts new file mode 100644 index 0000000000..21b4fcc06c --- /dev/null +++ b/packages/cli/src/lib/parsers/app.ts @@ -0,0 +1,46 @@ +import { intlMsg } from "../intl"; +import { defaultAppManifest } from "../manifest"; +import { resolvePathIfExists } from "../system"; + +import path from "path"; + +const defaultAppCodegenDir = "./src/w3"; + +export function parseAppManifestFileOption( + manifestFile: string | undefined, + _: unknown +): string { + const manifestPaths = manifestFile + ? [manifestFile as string] + : defaultAppManifest; + + manifestFile = resolvePathIfExists(manifestPaths); + + if (!manifestFile) { + console.error( + intlMsg.commands_build_error_manifestNotFound({ + paths: manifestPaths.join(", "), + }) + ); + process.exit(1); + } + + return manifestFile; +} + +export function defaultAppManifestFileOption(): string { + return parseAppManifestFileOption(undefined, undefined); +} + +export function parseAppCodegenDirOption( + codegenDir: string | undefined, + _: unknown +): string { + return codegenDir + ? path.resolve(codegenDir) + : path.resolve(defaultAppCodegenDir); +} + +export function defaultAppCodegenDirOption(): string { + return parseAppCodegenDirOption(undefined, undefined); +} diff --git a/packages/cli/src/lib/parsers/build.ts b/packages/cli/src/lib/parsers/build.ts new file mode 100644 index 0000000000..abd490a651 --- /dev/null +++ b/packages/cli/src/lib/parsers/build.ts @@ -0,0 +1,14 @@ +import path from "path"; + +const defaultBuildDir = "./build"; + +export function parseBuildOutputDirOption( + outputDir: string | undefined, + _: unknown +): string { + return outputDir ? path.resolve(outputDir) : path.resolve(defaultBuildDir); +} + +export function defaultBuildOutputDirOption(): string { + return parseBuildOutputDirOption(undefined, undefined); +} diff --git a/packages/cli/src/lib/parsers/codegen.ts b/packages/cli/src/lib/parsers/codegen.ts new file mode 100644 index 0000000000..7b3ed6f37d --- /dev/null +++ b/packages/cli/src/lib/parsers/codegen.ts @@ -0,0 +1,27 @@ +import path from "path"; + +const defaultCodegenDir = "./w3"; + +export function parseCodegenDirOption( + codegenDir: string | undefined, + _: unknown +): string { + return codegenDir + ? path.resolve(codegenDir) + : path.resolve(defaultCodegenDir); +} + +export function defaultCodegenDirOption(): string { + return parseCodegenDirOption(undefined, undefined); +} + +export function parseCodegenScriptOption( + script: string | undefined, + _: unknown +): string | undefined { + return script ? path.resolve(script) : undefined; +} + +export function defaultCodegenScriptOption(): string | undefined { + return parseCodegenScriptOption(undefined, undefined); +} diff --git a/packages/cli/src/lib/parsers/index.ts b/packages/cli/src/lib/parsers/index.ts new file mode 100644 index 0000000000..f1b70a6297 --- /dev/null +++ b/packages/cli/src/lib/parsers/index.ts @@ -0,0 +1,6 @@ +export * from "./app"; +export * from "./build"; +export * from "./codegen"; +export * from "./wasm"; +export * from "./plugin"; +export * from "./query"; diff --git a/packages/cli/src/lib/parsers/plugin.ts b/packages/cli/src/lib/parsers/plugin.ts new file mode 100644 index 0000000000..420d7258da --- /dev/null +++ b/packages/cli/src/lib/parsers/plugin.ts @@ -0,0 +1,59 @@ +import { intlMsg } from "../intl"; +import { defaultPluginManifest } from "../manifest"; +import { resolvePathIfExists } from "../system"; + +import path from "path"; + +const defaultPluginPublishDir = "./build"; +const defaultPluginCodegenDir = "./src/w3"; + +export function parsePluginManifestFileOption( + manifestFile: string | undefined, + _: unknown +): string { + const manifestPaths = manifestFile + ? [manifestFile as string] + : defaultPluginManifest; + manifestFile = resolvePathIfExists(manifestPaths); + + if (!manifestFile) { + console.error( + intlMsg.commands_build_error_manifestNotFound({ + paths: manifestPaths.join(", "), + }) + ); + process.exit(1); + } + + return manifestFile; +} + +export function defaultPluginManifestFileOption(): string { + return parsePluginManifestFileOption(undefined, undefined); +} + +export function parsePluginCodegenDirOption( + codegenDir: string | undefined, + _: unknown +): string { + return codegenDir + ? path.resolve(codegenDir) + : path.resolve(defaultPluginCodegenDir); +} + +export function defaultPluginCodegenDirOption(): string { + return parsePluginCodegenDirOption(undefined, undefined); +} + +export function parsePluginPublishDirOption( + publishDir: string | undefined, + _: unknown +): string { + return publishDir + ? path.resolve(publishDir) + : path.resolve(defaultPluginPublishDir); +} + +export function defaultPluginPublishDirOption(): string { + return parsePluginPublishDirOption(undefined, undefined); +} diff --git a/packages/cli/src/lib/parsers/query.ts b/packages/cli/src/lib/parsers/query.ts new file mode 100644 index 0000000000..a6210e63c5 --- /dev/null +++ b/packages/cli/src/lib/parsers/query.ts @@ -0,0 +1,88 @@ +import { intlMsg } from "../intl"; +import { importTypescriptModule } from "../system"; +import { getTestEnvClientConfig } from "../test-env"; +import { validateClientConfig } from "../helpers"; + +import path from "path"; +import fs from "fs"; +import { Web3ApiClientConfig } from "@web3api/client-js"; +import { executeMaybeAsyncFunction } from "@web3api/core-js"; + +export function parseRecipeScriptPathOption( + script: string, + _: unknown +): string { + const absPath = path.resolve(script); + if (!fs.existsSync(absPath)) { + throw new Error( + intlMsg.commands_query_error_noRecipeScriptFound({ path: absPath }) + ); + } + return absPath; +} + +export async function parseClientConfigOption( + _clientConfig: string | undefined, + _: unknown +): Promise> { + let finalClientConfig: Partial; + + try { + finalClientConfig = await getTestEnvClientConfig(); + } catch (e) { + console.error(intlMsg.commands_query_error_noTestEnvFound()); + process.exit(1); + } + + if (_clientConfig) { + let configModule; + + const configPath = path.resolve(_clientConfig); + if (configPath.endsWith(".js")) { + configModule = await import(path.resolve(configPath)); + } else if (configPath.endsWith(".ts")) { + configModule = await importTypescriptModule(path.resolve(configPath)); + } else { + const configsModuleMissingExportMessage = intlMsg.commands_query_error_clientConfigInvalidFileExt( + { module: configPath } + ); + console.error(configsModuleMissingExportMessage); + process.exit(1); + } + + if (!configModule || !configModule.getClientConfig) { + const configsModuleMissingExportMessage = intlMsg.commands_query_error_clientConfigModuleMissingExport( + { module: configModule } + ); + console.error(configsModuleMissingExportMessage); + process.exit(1); + } + + finalClientConfig = await executeMaybeAsyncFunction( + configModule.getClientConfig, + finalClientConfig + ); + + try { + validateClientConfig(finalClientConfig); + } catch (e) { + console.error(e.message); + process.exit(1); + } + } + + return finalClientConfig; +} + +export async function defaultClientConfigOption(): Promise< + Partial +> { + return await parseClientConfigOption(undefined, undefined); +} + +export function parseRecipeOutputFilePathOption( + outputFile: string, + _: unknown +): string { + return path.resolve(outputFile); +} diff --git a/packages/cli/src/lib/parsers/wasm.ts b/packages/cli/src/lib/parsers/wasm.ts new file mode 100644 index 0000000000..f15431a541 --- /dev/null +++ b/packages/cli/src/lib/parsers/wasm.ts @@ -0,0 +1,29 @@ +import { intlMsg } from "../intl"; +import { defaultWeb3ApiManifest } from "../manifest"; +import { resolvePathIfExists } from "../system"; + +export function parseWasmManifestFileOption( + manifestFile: string | undefined, + _: unknown +): string { + const manifestPaths = manifestFile + ? [manifestFile as string] + : defaultWeb3ApiManifest; + + manifestFile = resolvePathIfExists(manifestPaths); + + if (!manifestFile) { + console.error( + intlMsg.commands_build_error_manifestNotFound({ + paths: manifestPaths.join(", "), + }) + ); + process.exit(1); + } + + return manifestFile; +} + +export function defaultWasmManifestFileOption(): string { + return parseWasmManifestFileOption(undefined, undefined); +} diff --git a/packages/cli/src/lib/system/path.ts b/packages/cli/src/lib/system/path.ts index 22b76bcaaf..db4431bde0 100644 --- a/packages/cli/src/lib/system/path.ts +++ b/packages/cli/src/lib/system/path.ts @@ -1,17 +1,14 @@ import path from "path"; -import { GluegunFilesystem } from "gluegun"; +import fs from "fs"; export function displayPath(p: string): string { return "./" + path.relative(process.cwd(), p); } -export function resolvePathIfExists( - filesystem: GluegunFilesystem, - searchPaths: string[] -): string | undefined { +export function resolvePathIfExists(searchPaths: string[]): string | undefined { for (let i = 0; i < searchPaths.length; i++) { - if (filesystem.exists(searchPaths[i])) { - return filesystem.resolve(searchPaths[i]); + if (fs.existsSync(searchPaths[i])) { + return path.resolve(searchPaths[i]); } } return undefined; diff --git a/packages/test-cases/cases/cli/api/codegen/001-sanity/web3api-norun.gen.js b/packages/test-cases/cases/cli/api/codegen/001-sanity/web3api-norun.gen.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/test-cases/cases/cli/api/codegen/001-sanity/web3api.build.yaml b/packages/test-cases/cases/cli/api/codegen/001-sanity/web3api.build.yaml new file mode 100644 index 0000000000..2cf967618b --- /dev/null +++ b/packages/test-cases/cases/cli/api/codegen/001-sanity/web3api.build.yaml @@ -0,0 +1,6 @@ +format: 0.0.1-prealpha.2 +config: + node_version: "14.16.0" +linked_packages: + - name: "@web3api/wasm-as" + path: ../../../../../../wasm/as diff --git a/packages/test-cases/cases/cli/api/codegen/001-sanity/web3api.yaml b/packages/test-cases/cases/cli/api/codegen/001-sanity/web3api.yaml index 9a25b251fc..e7880e1e3a 100644 --- a/packages/test-cases/cases/cli/api/codegen/001-sanity/web3api.yaml +++ b/packages/test-cases/cases/cli/api/codegen/001-sanity/web3api.yaml @@ -1,5 +1,6 @@ format: 0.0.1-prealpha.7 name: test-project +build: ./web3api.build.yaml language: wasm/assemblyscript modules: mutation: diff --git a/packages/test-cases/cases/cli/api/codegen/002-invalid-codegen-script/web3api.build.yaml b/packages/test-cases/cases/cli/api/codegen/002-invalid-codegen-script/web3api.build.yaml new file mode 100644 index 0000000000..2cf967618b --- /dev/null +++ b/packages/test-cases/cases/cli/api/codegen/002-invalid-codegen-script/web3api.build.yaml @@ -0,0 +1,6 @@ +format: 0.0.1-prealpha.2 +config: + node_version: "14.16.0" +linked_packages: + - name: "@web3api/wasm-as" + path: ../../../../../../wasm/as diff --git a/packages/test-cases/cases/cli/api/codegen/002-invalid-codegen-script/web3api.yaml b/packages/test-cases/cases/cli/api/codegen/002-invalid-codegen-script/web3api.yaml index 9a25b251fc..e7880e1e3a 100644 --- a/packages/test-cases/cases/cli/api/codegen/002-invalid-codegen-script/web3api.yaml +++ b/packages/test-cases/cases/cli/api/codegen/002-invalid-codegen-script/web3api.yaml @@ -1,5 +1,6 @@ format: 0.0.1-prealpha.7 name: test-project +build: ./web3api.build.yaml language: wasm/assemblyscript modules: mutation: diff --git a/packages/test-cases/cases/cli/api/codegen/003-codegen-script/web3api.build.yaml b/packages/test-cases/cases/cli/api/codegen/003-codegen-script/web3api.build.yaml new file mode 100644 index 0000000000..2cf967618b --- /dev/null +++ b/packages/test-cases/cases/cli/api/codegen/003-codegen-script/web3api.build.yaml @@ -0,0 +1,6 @@ +format: 0.0.1-prealpha.2 +config: + node_version: "14.16.0" +linked_packages: + - name: "@web3api/wasm-as" + path: ../../../../../../wasm/as diff --git a/packages/test-cases/cases/cli/api/codegen/003-codegen-script/web3api.yaml b/packages/test-cases/cases/cli/api/codegen/003-codegen-script/web3api.yaml index 9a25b251fc..e7880e1e3a 100644 --- a/packages/test-cases/cases/cli/api/codegen/003-codegen-script/web3api.yaml +++ b/packages/test-cases/cases/cli/api/codegen/003-codegen-script/web3api.yaml @@ -1,5 +1,6 @@ format: 0.0.1-prealpha.7 name: test-project +build: ./web3api.build.yaml language: wasm/assemblyscript modules: mutation: diff --git a/packages/test-cases/cases/cli/api/query/recipes/output.json b/packages/test-cases/cases/cli/api/query/recipes/output.json deleted file mode 100644 index 5be6a59947..0000000000 --- a/packages/test-cases/cases/cli/api/query/recipes/output.json +++ /dev/null @@ -1 +0,0 @@ -[{"api":"ens/testnet/simplestorage.eth"},{"constants":"./constants.json"},{"query":"./set.graphql","variables":{"address":"$SimpleStorageAddr","value":569,"network":"testnet"},"output":{"data":{},"errors":[{}]}}] \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index fdb1cd8d17..bd356c316e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6932,6 +6932,11 @@ commander@3.0.2: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== +commander@9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.2.0.tgz#6e21014b2ed90d8b7c9647230d8b7a94a4a419a9" + integrity sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w== + commander@^2.11.0, commander@^2.15.0, commander@^2.19.0, commander@^2.20.0, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"