diff --git a/package.json b/package.json index b1ecc3e4f3..f36b92f792 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dependencies:install": "cd dependencies && yarn", "preinstall": "yarn dependencies:install", "build": "yarn build:core && yarn build:interfaces && yarn link:schema && yarn build:plugins && yarn build:resolver:plugins && yarn build:config && yarn build:core:client && yarn build:client && yarn build:test-env && yarn build:cli", - "build:core": "lerna run build --no-private --ignore @polywrap/*-plugin-js --ignore @polywrap/client-config-builder-js --ignore polywrap --ignore @polywrap/core-client-js --ignore @polywrap/client-js --ignore @polywrap/test-env-js --ignore @polywrap/*-interface", + "build:core": "lerna run build --no-private --ignore @polywrap/*-plugin-js --ignore @polywrap/client-config-builder-js --ignore polywrap --ignore @polywrap/core-client-js --ignore @polywrap/client-js --ignore @polywrap/test-env-js --ignore @polywrap/*-interface --ignore @polywrap/cli-js", "build:interfaces": "lerna run build --scope @polywrap/*-interface", "build:plugins": "lerna run build --scope @polywrap/*-plugin-js --ignore @polywrap/*-resolver-plugin-js", "build:resolver:plugins": "lerna run build --scope @polywrap/*-resolver-plugin-js", @@ -34,7 +34,7 @@ "build:core:client": "lerna run build --scope @polywrap/core-client-js", "build:client": "lerna run build --scope @polywrap/client-js", "build:test-env": "lerna run build --scope @polywrap/test-env-js", - "build:cli": "lerna run build --scope polywrap", + "build:cli": "lerna run build --scope polywrap && lerna run build --scope @polywrap/cli-js", "link:interface:deps": "yarn link:manifests && yarn link:schema", "link:manifests": "yarn link:manifests:polywrap && yarn link:manifests:wrap", "link:manifests:polywrap": "cd packages/js/manifests/polywrap && (yarn unlink || true) && yarn link && cd ../../../../dependencies && yarn link @polywrap/polywrap-manifest-types-js && cd ../", diff --git a/packages/cli/lang/en.json b/packages/cli/lang/en.json index 2281a0c78f..4de8cded97 100644 --- a/packages/cli/lang/en.json +++ b/packages/cli/lang/en.json @@ -21,7 +21,7 @@ "commands_build_options_codegen": "Perform code generation before build", "commands_build_options_codegen_dir": "Codegen output directory (default: {default})", "commands_build_options_w": "Automatically rebuild when changes are made (default: false)", - "commands_build_options_s": "Strategy to use for building the wrapper", + "commands_build_options_s": "Strategy to use for building the wrapper (default: {default})", "commands_build_options_s_strategy": "strategy", "commands_build_options_l": "Log file to save console output to", "commands_build_error_codegen_failed": "Code generation failed", diff --git a/packages/cli/lang/es.json b/packages/cli/lang/es.json index 2281a0c78f..4de8cded97 100644 --- a/packages/cli/lang/es.json +++ b/packages/cli/lang/es.json @@ -21,7 +21,7 @@ "commands_build_options_codegen": "Perform code generation before build", "commands_build_options_codegen_dir": "Codegen output directory (default: {default})", "commands_build_options_w": "Automatically rebuild when changes are made (default: false)", - "commands_build_options_s": "Strategy to use for building the wrapper", + "commands_build_options_s": "Strategy to use for building the wrapper (default: {default})", "commands_build_options_s_strategy": "strategy", "commands_build_options_l": "Log file to save console output to", "commands_build_error_codegen_failed": "Code generation failed", diff --git a/packages/cli/package.json b/packages/cli/package.json index 9400211110..1fdfe9f082 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -15,6 +15,8 @@ "bin": { "polywrap": "bin/polywrap" }, + "main": "build/index.js", + "types": "build/types/index.d.ts", "scripts": { "build": "yarn build:intl && yarn build:fast", "build:fast": "rimraf ./build && tsc --project tsconfig.build.json && yarn build:build-strategies && yarn build:deploy-modules && yarn build:infra-modules && yarn build:docgen-templates", diff --git a/packages/cli/src/__tests__/e2e/build-rs.spec.ts b/packages/cli/src/__tests__/e2e/build-rs.spec.ts index 0238e7a15b..a2c767b6c9 100644 --- a/packages/cli/src/__tests__/e2e/build-rs.spec.ts +++ b/packages/cli/src/__tests__/e2e/build-rs.spec.ts @@ -5,7 +5,7 @@ import { GetPathToCliTestFiles } from "@polywrap/test-cases"; import fs from "fs"; import path from "path"; -jest.setTimeout(700000); +jest.setTimeout(1200000); describe("e2e tests for build command", () => { const testCaseRoot = path.join(GetPathToCliTestFiles(), "wasm/build-cmd/rust"); diff --git a/packages/cli/src/__tests__/e2e/build.wasm.spec.ts b/packages/cli/src/__tests__/e2e/build.wasm.spec.ts index e5e8bbb8fe..f15b9eb65e 100644 --- a/packages/cli/src/__tests__/e2e/build.wasm.spec.ts +++ b/packages/cli/src/__tests__/e2e/build.wasm.spec.ts @@ -12,25 +12,25 @@ const HELP = `Usage: polywrap build|b [options] Build Polywrap Projects (type: interface, wasm) Options: - -m, --manifest-file Path to the Polywrap Build manifest file - (default: polywrap.yaml | polywrap.yml) - -o, --output-dir Output directory for build results - (default: ./build) - -c, --client-config Add custom configuration to the - PolywrapClient - --codegen Perform code generation before build - --codegen-dir Codegen output directory (default: - ./src/wrap) - --wrapper-envs Path to a JSON file containing wrapper - envs - -s, --strategy Strategy to use for building the wrapper - (default: "vm") - -w, --watch Automatically rebuild when changes are - made (default: false) - -v, --verbose Verbose output (default: false) - -q, --quiet Suppress output (default: false) - -l, --log-file [path] Log file to save console output to - -h, --help display help for command + -m, --manifest-file Path to the Polywrap Build manifest file + (default: polywrap.yaml | polywrap.yml) + -o, --output-dir Output directory for build results + (default: ./build) + -c, --client-config Add custom configuration to the + PolywrapClient + --codegen Perform code generation before build + --codegen-dir Codegen output directory (default: + ./src/wrap) + --wrapper-envs Path to a JSON file containing wrapper + envs + -s, --strategy Strategy to use for building the wrapper + (default: vm) + -w, --watch Automatically rebuild when changes are + made (default: false) + -v, --verbose Verbose output (default: false) + -q, --quiet Suppress output (default: false) + -l, --log-file [path] Log file to save console output to + -h, --help display help for command `; jest.setTimeout(500000); @@ -228,7 +228,7 @@ describe("e2e tests for build command", () => { beforeAll(async () => { await cleanupYarnLockfiles(); }); - + afterAll(async () => { await cleanupYarnLockfiles(); }); diff --git a/packages/cli/src/__tests__/e2e/codegen.spec.ts b/packages/cli/src/__tests__/e2e/codegen.spec.ts index d57b011a05..d357bd254c 100644 --- a/packages/cli/src/__tests__/e2e/codegen.spec.ts +++ b/packages/cli/src/__tests__/e2e/codegen.spec.ts @@ -14,7 +14,7 @@ Generate Code For Polywrap Projects Options: -m, --manifest-file Path to the Polywrap manifest file (default: polywrap.yaml | polywrap.yml) - -g, --codegen-dir Output directory for the generated code + -g, --codegen-dir Output directory for the generated code (default: ./src/wrap) -s, --script Path to a custom generation script (JavaScript | TypeScript) diff --git a/packages/cli/src/__tests__/e2e/help.spec.ts b/packages/cli/src/__tests__/e2e/help.spec.ts index 455940204e..31560e6802 100644 --- a/packages/cli/src/__tests__/e2e/help.spec.ts +++ b/packages/cli/src/__tests__/e2e/help.spec.ts @@ -13,10 +13,10 @@ Commands: codegen|g [options] Generate Code For Polywrap Projects create|c Create New Projects deploy|d [options] Deploys Polywrap Projects - infra|i [options] Modular Infrastructure-As-Code Orchestrator - test|t [options] Execute Tests docgen|o [options] Generate wrapper documentation + infra|i [options] Modular Infrastructure-As-Code Orchestrator manifest|m Inspect & Migrade Polywrap Manifests + test|t [options] Execute Tests help [command] display help for command `; diff --git a/packages/cli/src/__tests__/e2e/infra.spec.ts b/packages/cli/src/__tests__/e2e/infra.spec.ts index 86328ed8c1..f844e49f3e 100644 --- a/packages/cli/src/__tests__/e2e/infra.spec.ts +++ b/packages/cli/src/__tests__/e2e/infra.spec.ts @@ -19,7 +19,7 @@ const HELP = `Usage: polywrap infra|i [options] Modular Infrastructure-As-Code Orchestrator Arguments: - action + action Infra allows you to execute the following commands: up Start Polywrap infrastructure down Stop Polywrap infrastructure @@ -28,14 +28,14 @@ Arguments: (choices: "up", "down", "vars", "config") Options: - -m, --manifest-file Path to the Polywrap Infra manifest file - (default: polywrap.infra.yaml | - polywrap.infra.yml) - -o, --modules Use only specified modules - -v, --verbose Verbose output (default: false) - -q, --quiet Suppress output (default: false) - -l, --log-file [path] Log file to save console output to - -h, --help display help for command + -m, --manifest-file Path to the Polywrap Infra manifest file + (default: polywrap.infra.yaml | + polywrap.infra.yml) + -o, --modules Use only specified modules + -v, --verbose Verbose output (default: false) + -q, --quiet Suppress output (default: false) + -l, --log-file [path] Log file to save console output to + -h, --help display help for command `; const portInUse = (port: number) => { @@ -119,7 +119,7 @@ describe("e2e tests for infra command", () => { ); await runPolywrapCli( - ["infra", "down", "-v", "--modules=eth-ens-ipfs"], + ["infra", "down", "-v", "--modules eth-ens-ipfs"], getTestCaseDir(0), ); @@ -222,7 +222,7 @@ describe("e2e tests for infra command", () => { it("Should correctly open one process for default module because modules flag overwrites it", async () => { await runPolywrapCli( - ["infra", "up", "--modules=eth-ens-ipfs"], + ["infra", "up", "--modules eth-ens-ipfs"], getTestCaseDir(4), ); @@ -231,7 +231,7 @@ describe("e2e tests for infra command", () => { ]); await runPolywrapCli( - ["infra", "down", "--modules=eth-ens-ipfs"], + ["infra", "down", "--modules eth-ens-ipfs"], getTestCaseDir(4), ); }); @@ -291,7 +291,7 @@ describe("e2e tests for infra command", () => { it("Should set environment up with only selected modules", async () => { await runPolywrapCli( - ["infra", "up", "--modules=ipfs"], + ["infra", "up", "--modules ipfs"], getTestCaseDir(0), ); @@ -302,7 +302,7 @@ describe("e2e tests for infra command", () => { ]); await runPolywrapCli( - ["infra", "down", "--modules=ipfs"], + ["infra", "down", "--modules ipfs"], getTestCaseDir(0), ); @@ -316,7 +316,7 @@ describe("e2e tests for infra command", () => { [ "infra", "config", - "--modules=notExistingModule,alsoNotExisting", + "--modules notExistingModule alsoNotExisting", ], getTestCaseDir(0), ); @@ -332,7 +332,7 @@ describe("e2e tests for infra command", () => { [ "infra", "up", - "--modules=eth-ens-ipfs", + "--modules eth-ens-ipfs", "--verbose" ], getTestCaseDir(0), @@ -349,7 +349,7 @@ describe("e2e tests for infra command", () => { [ "infra", "config", - "--modules=eth-ens-ipfs", + "--modules eth-ens-ipfs", "--verbose" ], getTestCaseDir(2), @@ -363,7 +363,7 @@ describe("e2e tests for infra command", () => { [ "infra", "config", - "--modules=eth-ens-ipfs", + "--modules eth-ens-ipfs", "--verbose" ], getTestCaseDir(0), @@ -380,7 +380,7 @@ describe("e2e tests for infra command", () => { [ "infra", "up", - "--modules=eth-ens-ipfs", + "--modules eth-ens-ipfs", "--verbose" ], getTestCaseDir(3), @@ -430,7 +430,7 @@ describe("e2e tests for infra command", () => { [ "infra", "up", - "--modules=ganache,dev-server" + "--modules ganache,dev-server" ], getTestCaseDir(1), ); @@ -441,7 +441,7 @@ describe("e2e tests for infra command", () => { ]); await runPolywrapCli( - ["infra", "down", "--modules=ganache,dev-server"], + ["infra", "down", "--modules ganache,dev-server"], getTestCaseDir(1), ); }); @@ -451,7 +451,7 @@ describe("e2e tests for infra command", () => { [ "infra", "up", - "--modules=ipfs,ipfs-duplicate" + "--modules ipfs,ipfs-duplicate" ], getTestCaseDir(1), ); @@ -461,7 +461,7 @@ describe("e2e tests for infra command", () => { ]); await runPolywrapCli( - ["infra", "down", "--modules=ipfs,ipfs-duplicate"], + ["infra", "down", "--modules ipfs,ipfs-duplicate"], getTestCaseDir(1), ); }); diff --git a/packages/cli/src/__tests__/e2e/manifest.spec.ts b/packages/cli/src/__tests__/e2e/manifest.spec.ts index 889da7c833..65cb84a455 100644 --- a/packages/cli/src/__tests__/e2e/manifest.spec.ts +++ b/packages/cli/src/__tests__/e2e/manifest.spec.ts @@ -51,7 +51,7 @@ Arguments: \"infra\", \"workflow\", default: \"project\") Options: - -r, --raw Output raw JSON Schema (default: false) + -r, --raw Output raw JSON Schema -m, --manifest-file Path to the manifest file (default: polywrap.yaml | polywrap.yml) -v, --verbose Verbose output (default: false) diff --git a/packages/cli/src/__tests__/e2e/no-command.spec.ts b/packages/cli/src/__tests__/e2e/no-command.spec.ts index c870cb2ee6..d1d3204cbc 100644 --- a/packages/cli/src/__tests__/e2e/no-command.spec.ts +++ b/packages/cli/src/__tests__/e2e/no-command.spec.ts @@ -13,10 +13,10 @@ Commands: codegen|g [options] Generate Code For Polywrap Projects create|c Create New Projects deploy|d [options] Deploys Polywrap Projects - infra|i [options] Modular Infrastructure-As-Code Orchestrator - test|t [options] Execute Tests docgen|o [options] Generate wrapper documentation + infra|i [options] Modular Infrastructure-As-Code Orchestrator manifest|m Inspect & Migrade Polywrap Manifests + test|t [options] Execute Tests help [command] display help for command `; diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index f77fc33b75..20fc78614e 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -1,4 +1,4 @@ -import { Command, Program } from "./types"; +import { Command, Program, BaseCommandOptions } from "./types"; import { createLogger } from "./utils/createLogger"; import { Compiler, @@ -32,11 +32,11 @@ import { defaultCodegenDir } from "../lib/defaults/defaultCodegenDir"; import readline from "readline"; import { Env, PolywrapClient } from "@polywrap/client-js"; import { PolywrapManifest } from "@polywrap/polywrap-manifest-types-js"; -import { IClientConfigBuilder } from "@polywrap/client-config-builder-js"; +import { Uri } from "@polywrap/core-js"; const defaultOutputDir = "./build"; const defaultStrategy = SupportedStrategies.VM; -const strategyStr = intlMsg.commands_build_options_s_strategy(); +const strategyStr = Object.values(SupportedStrategies).join(" | "); const defaultManifestStr = defaultPolywrapManifest.join(" | "); const pathStr = intlMsg.commands_build_options_o_path(); @@ -45,19 +45,16 @@ const supportedProjectTypes = [ ...Object.values(pluginManifestLanguages), ]; -type BuildCommandOptions = { +export interface BuildCommandOptions extends BaseCommandOptions { manifestFile: string; outputDir: string; - configBuilder: IClientConfigBuilder; + clientConfig: string | false; + wrapperEnvs: string | false; codegen: boolean; // defaults to false codegenDir: string; - wrapperEnvs: Env[]; - watch?: boolean; - strategy: SupportedStrategies; - verbose?: boolean; - quiet?: boolean; - logFile?: string; -}; + watch: boolean; + strategy: `${SupportedStrategies}`; +} export const build: Command = { setup: (program: Program) => { @@ -94,8 +91,9 @@ export const build: Command = { ) .option( `-s, --strategy <${strategyStr}>`, - `${intlMsg.commands_build_options_s()}`, - defaultStrategy + `${intlMsg.commands_build_options_s({ + default: defaultStrategy, + })}` ) .option(`-w, --watch`, `${intlMsg.commands_build_options_w()}`) .option("-v, --verbose", intlMsg.commands_common_options_verbose()) @@ -104,18 +102,21 @@ export const build: Command = { `-l, --log-file [${pathStr}]`, `${intlMsg.commands_build_options_l()}` ) - .action(async (options) => { + .action(async (options: Partial) => { await run({ - ...options, manifestFile: parseManifestFileOption( options.manifestFile, defaultPolywrapManifest ), - configBuilder: await parseClientConfigOption(options.clientConfig), - wrapperEnvs: await parseWrapperEnvsOption(options.wrapperEnvs), + clientConfig: options.clientConfig || false, + wrapperEnvs: options.wrapperEnvs || false, outputDir: parseDirOption(options.outputDir, defaultOutputDir), + codegen: options.codegen || false, codegenDir: parseDirOption(options.codegenDir, defaultCodegenDir), - strategy: options.strategy, + strategy: options.strategy || defaultStrategy, + watch: options.watch || false, + verbose: options.verbose || false, + quiet: options.quiet || false, logFile: parseLogFileOption(options.logFile), }); }); @@ -157,13 +158,13 @@ function createBuildStrategy( } } -async function run(options: BuildCommandOptions) { +async function run(options: Required) { const { watch, manifestFile, - outputDir, - configBuilder, + clientConfig, wrapperEnvs, + outputDir, strategy, codegen, codegenDir, @@ -173,8 +174,11 @@ async function run(options: BuildCommandOptions) { } = options; const logger = createLogger({ verbose, quiet, logFile }); - if (wrapperEnvs) { - configBuilder.addEnvs(wrapperEnvs); + const envs = await parseWrapperEnvsOption(wrapperEnvs); + const configBuilder = await parseClientConfigOption(clientConfig); + + if (envs) { + configBuilder.addEnvs(envs as Env[]); } // Get Client diff --git a/packages/cli/src/commands/codegen.ts b/packages/cli/src/commands/codegen.ts index 1b29c7e9a5..aa233602fc 100644 --- a/packages/cli/src/commands/codegen.ts +++ b/packages/cli/src/commands/codegen.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Command, Program } from "./types"; +import { Command, Program, BaseCommandOptions } from "./types"; import { createLogger } from "./utils/createLogger"; import { CodeGenerator, @@ -19,21 +19,18 @@ import { ScriptCodegenerator } from "../lib/codegen/ScriptCodeGenerator"; import { defaultCodegenDir } from "../lib/defaults/defaultCodegenDir"; import { Env, PolywrapClient } from "@polywrap/client-js"; -import { IClientConfigBuilder } from "@polywrap/client-config-builder-js"; +import { Uri } from "@polywrap/core-js"; const pathStr = intlMsg.commands_codegen_options_o_path(); const defaultManifestStr = defaultPolywrapManifest.join(" | "); -type CodegenCommandOptions = { +export interface CodegenCommandOptions extends BaseCommandOptions { manifestFile: string; codegenDir: string; - script?: string; - configBuilder: IClientConfigBuilder; - wrapperEnvs: Env[]; - verbose?: boolean; - quiet?: boolean; - logFile?: string; -}; + script: string | false; + clientConfig: string | false; + wrapperEnvs: string | false; +} export const codegen: Command = { setup: (program: Program) => { @@ -49,7 +46,7 @@ export const codegen: Command = { ) .option( `-g, --codegen-dir <${pathStr}>`, - ` ${intlMsg.commands_codegen_options_codegen({ + `${intlMsg.commands_codegen_options_codegen({ default: defaultCodegenDir, })}` ) @@ -71,38 +68,42 @@ export const codegen: Command = { `-l, --log-file [${pathStr}]`, `${intlMsg.commands_build_options_l()}` ) - .action(async (options) => { + .action(async (options: Partial) => { await run({ - ...options, - configBuilder: await parseClientConfigOption(options.clientConfig), - wrapperEnvs: await parseWrapperEnvsOption(options.wrapperEnvs), - codegenDir: parseDirOption(options.codegenDir, defaultCodegenDir), - script: parseCodegenScriptOption(options.script), manifestFile: parseManifestFileOption( options.manifestFile, defaultProjectManifestFiles ), + codegenDir: parseDirOption(options.codegenDir, defaultCodegenDir), + script: parseCodegenScriptOption(options.script), + clientConfig: options.clientConfig || false, + wrapperEnvs: options.wrapperEnvs || false, + verbose: options.verbose || false, + quiet: options.quiet || false, logFile: parseLogFileOption(options.logFile), }); }); }, }; -async function run(options: CodegenCommandOptions) { +async function run(options: Required) { const { manifestFile, + clientConfig, + wrapperEnvs, codegenDir, script, - configBuilder, - wrapperEnvs, verbose, quiet, logFile, } = options; const logger = createLogger({ verbose, quiet, logFile }); - if (wrapperEnvs) { - configBuilder.addEnvs(wrapperEnvs); + const envs = await parseWrapperEnvsOption(wrapperEnvs); + const configBuilder = await parseClientConfigOption(clientConfig); + + if (envs) { + configBuilder.addEnvs(envs as Env[]); } // Get Client diff --git a/packages/cli/src/commands/create.ts b/packages/cli/src/commands/create.ts index e3ca293a32..953a8f80a2 100644 --- a/packages/cli/src/commands/create.ts +++ b/packages/cli/src/commands/create.ts @@ -1,4 +1,4 @@ -import { Command, Program } from "./types"; +import { Command, Program, BaseCommandOptions } from "./types"; import { createLogger } from "./utils/createLogger"; import { generateProjectTemplate, intlMsg, parseLogFileOption } from "../lib"; @@ -23,13 +23,17 @@ export const supportedLangs = { }; export type ProjectType = keyof typeof supportedLangs; -export type SupportedLangs = typeof supportedLangs[ProjectType][number]; -type CreateCommandOptions = { - outputDir?: string; - verbose?: boolean; - quiet?: boolean; - logFile?: string; -}; +export type SupportedWasmLangs = typeof supportedLangs.wasm[number]; +export type SupportedAppLangs = typeof supportedLangs.app[number]; +export type SupportedPluginLangs = typeof supportedLangs.plugin[number]; +type SupportedLangs = + | SupportedWasmLangs + | SupportedAppLangs + | SupportedPluginLangs; + +export interface CreateCommandOptions extends BaseCommandOptions { + outputDir: string | false; +} export const create: Command = { setup: (program: Program) => { @@ -59,12 +63,16 @@ export const create: Command = { `-l, --log-file [${pathStr}]`, `${intlMsg.commands_build_options_l()}` ) - .action(async (langStr, nameStr, options) => { - await run("wasm", langStr, nameStr, { - ...options, - logFile: parseLogFileOption(options.logFile), - }); - }); + .action( + async (language, name, options: Partial) => { + await run("wasm", language, name, { + outputDir: options.outputDir || false, + verbose: options.verbose || false, + quiet: options.quiet || false, + logFile: parseLogFileOption(options.logFile), + }); + } + ); createCommand .command("app") @@ -87,12 +95,16 @@ export const create: Command = { `-l, --log-file [${pathStr}]`, `${intlMsg.commands_build_options_l()}` ) - .action(async (langStr, nameStr, options) => { - await run("app", langStr, nameStr, { - ...options, - logFile: parseLogFileOption(options.logFile), - }); - }); + .action( + async (language, name, options: Partial) => { + await run("app", language, name, { + outputDir: options.outputDir || false, + verbose: options.verbose || false, + quiet: options.quiet || false, + logFile: parseLogFileOption(options.logFile), + }); + } + ); createCommand .command(`plugin`) @@ -115,20 +127,24 @@ export const create: Command = { `-l, --log-file [${pathStr}]`, `${intlMsg.commands_build_options_l()}` ) - .action(async (langStr, nameStr, options) => { - await run("plugin", langStr, nameStr, { - ...options, - logFile: parseLogFileOption(options.logFile), - }); - }); + .action( + async (language, name, options: Partial) => { + await run("plugin", language, name, { + outputDir: options.outputDir || false, + verbose: options.verbose || false, + quiet: options.quiet || false, + logFile: parseLogFileOption(options.logFile), + }); + } + ); }, }; async function run( command: ProjectType, - lang: SupportedLangs, + language: SupportedLangs, name: string, - options: CreateCommandOptions + options: Required ) { const { outputDir, verbose, quiet, logFile } = options; const logger = createLogger({ verbose, quiet, logFile }); @@ -158,7 +174,7 @@ async function run( } } - await generateProjectTemplate(command, lang, projectDir) + await generateProjectTemplate(command, language, projectDir) .then(() => { let readyMessage; if (command === "wasm") { diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index af39d437ef..01f635a6fc 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -1,5 +1,5 @@ /* eslint-disable prefer-const */ -import { Command, Program } from "./types"; +import { Command, Program, BaseCommandOptions } from "./types"; import { createLogger } from "./utils/createLogger"; import { defaultPolywrapManifest, @@ -21,13 +21,10 @@ import { validate } from "jsonschema"; const defaultManifestStr = defaultPolywrapManifest.join(" | "); const pathStr = intlMsg.commands_deploy_options_o_path(); -type DeployCommandOptions = { +export interface DeployCommandOptions extends BaseCommandOptions { manifestFile: string; - outputFile?: string; - verbose?: boolean; - quiet?: boolean; - logFile?: string; -}; + outputFile: string | false; +} type ManifestJob = DeployManifest["jobs"][number]; type ManifestStep = ManifestJob["steps"][number]; @@ -54,20 +51,22 @@ export const deploy: Command = { `-l, --log-file [${pathStr}]`, `${intlMsg.commands_build_options_l()}` ) - .action(async (options) => { + .action(async (options: Partial) => { await run({ - ...options, manifestFile: parseManifestFileOption( options.manifestFile, defaultPolywrapManifest ), + outputFile: options.outputFile || false, + verbose: options.verbose || false, + quiet: options.quiet || false, logFile: parseLogFileOption(options.logFile), }); }); }, }; -async function run(options: DeployCommandOptions): Promise { +async function run(options: Required): Promise { const { manifestFile, outputFile, verbose, quiet, logFile } = options; const logger = createLogger({ verbose, quiet, logFile }); diff --git a/packages/cli/src/commands/docgen.ts b/packages/cli/src/commands/docgen.ts index c18f90230d..62f50ce0ee 100644 --- a/packages/cli/src/commands/docgen.ts +++ b/packages/cli/src/commands/docgen.ts @@ -11,7 +11,7 @@ import { parseLogFileOption, parseWrapperEnvsOption, } from "../lib"; -import { Command, Program } from "./types"; +import { Command, Program, BaseCommandOptions } from "./types"; import { createLogger } from "./utils/createLogger"; import { scriptPath as docusaurusScriptPath } from "../lib/docgen/docusaurus"; import { scriptPath as jsdocScriptPath } from "../lib/docgen/jsdoc"; @@ -21,7 +21,7 @@ import { ScriptCodegenerator } from "../lib/codegen/ScriptCodeGenerator"; import { Env, PolywrapClient } from "@polywrap/client-js"; import chalk from "chalk"; import { Argument } from "commander"; -import { IClientConfigBuilder } from "@polywrap/client-config-builder-js"; +import { Uri } from "@polywrap/core-js"; const commandToPathMap: Record = { schema: schemaScriptPath, @@ -29,37 +29,34 @@ const commandToPathMap: Record = { jsdoc: jsdocScriptPath, }; -export type DocType = keyof typeof commandToPathMap; - const defaultDocgenDir = "./docs"; const pathStr = intlMsg.commands_codegen_options_o_path(); -type DocgenCommandOptions = { - manifestFile: string; - docgenDir: string; - configBuilder: IClientConfigBuilder; - wrapperEnvs: Env[]; - imports: boolean; - verbose?: boolean; - quiet?: boolean; - logFile?: string; -}; - -enum Actions { +export enum DocgenActions { SCHEMA = "schema", DOCUSAURUS = "docusaurus", JSDOC = "jsdoc", } +export interface DocgenCommandOptions extends BaseCommandOptions { + manifestFile: string; + docgenDir: string; + clientConfig: string | false; + wrapperEnvs: string | false; + imports: boolean; +} + const argumentsDescription = ` - ${chalk.bold(Actions.SCHEMA)} ${intlMsg.commands_docgen_options_schema()} ${chalk.bold( - Actions.DOCUSAURUS + DocgenActions.SCHEMA + )} ${intlMsg.commands_docgen_options_schema()} + ${chalk.bold( + DocgenActions.DOCUSAURUS )} ${intlMsg.commands_docgen_options_markdown({ framework: "Docusaurus", })} ${chalk.bold( - Actions.JSDOC + DocgenActions.JSDOC )} ${intlMsg.commands_docgen_options_markdown({ framework: "JSDoc", })} @@ -74,9 +71,9 @@ export const docgen: Command = { .usage(" [options]") .addArgument( new Argument("", argumentsDescription).choices([ - Actions.SCHEMA, - Actions.DOCUSAURUS, - Actions.JSDOC, + DocgenActions.SCHEMA, + DocgenActions.DOCUSAURUS, + DocgenActions.JSDOC, ]) ) .option( @@ -106,28 +103,33 @@ export const docgen: Command = { `-l, --log-file [${pathStr}]`, `${intlMsg.commands_build_options_l()}` ) - .action(async (action, options) => { + .action(async (action, options: Partial) => { await run(action, { - ...options, manifestFile: parseManifestFileOption( options.manifestFile, defaultProjectManifestFiles ), docgenDir: parseDirOption(options.docgenDir, defaultDocgenDir), - configBuilder: await parseClientConfigOption(options.clientConfig), - wrapperEnvs: await parseWrapperEnvsOption(options.wrapperEnvs), + clientConfig: options.clientConfig || false, + wrapperEnvs: options.wrapperEnvs || false, + imports: options.imports || false, + verbose: options.verbose || false, + quiet: options.quiet || false, logFile: parseLogFileOption(options.logFile), }); }); }, }; -async function run(command: DocType, options: DocgenCommandOptions) { +async function run( + action: DocgenActions, + options: Required +) { const { manifestFile, - docgenDir, - configBuilder, + clientConfig, wrapperEnvs, + docgenDir, imports, verbose, quiet, @@ -135,8 +137,11 @@ async function run(command: DocType, options: DocgenCommandOptions) { } = options; const logger = createLogger({ verbose, quiet, logFile }); - if (wrapperEnvs) { - configBuilder.addEnvs(wrapperEnvs); + const envs = await parseWrapperEnvsOption(wrapperEnvs); + const configBuilder = await parseClientConfigOption(clientConfig); + + if (envs) { + configBuilder.addEnvs(envs as Env[]); } let project = await getProjectFromManifest(manifestFile, logger); @@ -154,7 +159,7 @@ async function run(command: DocType, options: DocgenCommandOptions) { await project.validate(); // Resolve custom script - const customScript = require.resolve(commandToPathMap[command]); + const customScript = require.resolve(commandToPathMap[action]); const client = new PolywrapClient(configBuilder.buildCoreConfig(), { noDefaults: true, diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 978a0c4149..768450707f 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -2,7 +2,65 @@ export * from "./build"; export * from "./codegen"; export * from "./create"; export * from "./deploy"; -export * from "./infra"; -export * from "./test"; export * from "./docgen"; +export * from "./infra"; export * from "./manifest"; +export * from "./test"; +export * from "./types"; + +import { BuildCommandOptions } from "./build"; +import { CodegenCommandOptions } from "./codegen"; +import { + CreateCommandOptions, + SupportedAppLangs, + SupportedPluginLangs, + SupportedWasmLangs, +} from "./create"; +import { DeployCommandOptions } from "./deploy"; +import { DocgenCommandOptions, DocgenActions } from "./docgen"; +import { InfraCommandOptions, InfraActions } from "./infra"; +import { + ManifestSchemaCommandOptions, + ManifestMigrateCommandOptions, + ManifestType, +} from "./manifest"; +import { TestCommandOptions } from "./test"; + +export interface CommandTypings { + build: BuildCommandOptions; + codegen: CodegenCommandOptions; + create: { + app: { + options: CreateCommandOptions; + arguments: [language: SupportedAppLangs, name: string]; + }; + plugin: { + options: CreateCommandOptions; + arguments: [language: SupportedPluginLangs, name: string]; + }; + wasm: { + options: CreateCommandOptions; + arguments: [language: SupportedWasmLangs, name: string]; + }; + }; + deploy: DeployCommandOptions; + docgen: { + options: DocgenCommandOptions; + arguments: [action: `${DocgenActions}`]; + }; + infra: { + options: InfraCommandOptions; + arguments: [action: `${InfraActions}`]; + }; + manifest: { + migrate: { + options: ManifestMigrateCommandOptions; + arguments: [type: ManifestType]; + }; + schema: { + options: ManifestSchemaCommandOptions; + arguments: [type: ManifestType]; + }; + }; + test: TestCommandOptions; +} diff --git a/packages/cli/src/commands/infra.ts b/packages/cli/src/commands/infra.ts index 2bd2c4ed35..3841bcc13d 100644 --- a/packages/cli/src/commands/infra.ts +++ b/packages/cli/src/commands/infra.ts @@ -7,7 +7,7 @@ import { parseLogFileOption, } from "../lib"; import { createLogger } from "./utils/createLogger"; -import { Command, Program } from "./types"; +import { Command, Program, BaseCommandOptions } from "./types"; import { InfraManifest } from "@polywrap/polywrap-manifest-types-js"; import path from "path"; @@ -16,21 +16,18 @@ import chalk from "chalk"; import yaml from "yaml"; import { readdirSync } from "fs"; -type InfraCommandOptions = { - modules?: string; - verbose?: boolean; - quiet?: boolean; - manifest: string; - logFile?: string; -}; - -enum InfraActions { +export enum InfraActions { UP = "up", DOWN = "down", VARS = "vars", CONFIG = "config", } +export interface InfraCommandOptions extends BaseCommandOptions { + manifestFile: string | false; + modules: string[] | false; +} + const DEFAULT_MODULES_PATH = path.join( __dirname, "..", @@ -76,7 +73,7 @@ export const infra: Command = { }) ) .option( - `-o, --modules <${moduleNameStr},${moduleNameStr}>`, + `-o, --modules <${moduleNameStr}...>`, intlMsg.commands_infra_options_o() ) .option("-v, --verbose", intlMsg.commands_common_options_verbose()) @@ -85,12 +82,12 @@ export const infra: Command = { `-l, --log-file [${pathStr}]`, `${intlMsg.commands_build_options_l()}` ) - .action(async (action, options) => { + .action(async (action, options: Partial) => { await run(action, { - ...options, - manifest: options.manifestFile - ? [options.manifestFile] - : defaultInfraManifest, + manifestFile: options.manifestFile || false, + modules: options.modules || false, + verbose: options.verbose || false, + quiet: options.quiet || false, logFile: parseLogFileOption(options.logFile), }); }); @@ -107,18 +104,22 @@ Example: 'polywrap infra up --modules=eth-ens-ipfs'.`; async function run( action: InfraActions, - options: InfraCommandOptions & { manifest: string[] } + options: Required ): Promise { - const { modules, verbose, quiet, manifest, logFile } = options; + const { modules, verbose, quiet, manifestFile, logFile } = options; const logger = createLogger({ verbose, quiet, logFile }); - // eslint-disable-next-line prefer-const - let modulesArray: string[] = []; + const modulesArray: string[] = []; if (modules) { - modulesArray = modules.split(",").map((m: string) => m.trim()); + modules.forEach((x) => + modulesArray.push(...(x.includes(",") ? x.split(",") : [x])) + ); } + const manifest: string[] = manifestFile + ? [manifestFile] + : defaultInfraManifest; const manifestPath = resolvePathIfExists(manifest); let infraManifest: InfraManifest | undefined; @@ -150,7 +151,7 @@ async function run( if (!filteredModules.length) { if (modules) { const errorMsg = intlMsg.commands_infra_error_noModulesMatch({ - modules, + modules: modules.join(", "), }); logger.error(errorMsg); return; diff --git a/packages/cli/src/commands/manifest.ts b/packages/cli/src/commands/manifest.ts index 7f68457213..113adaea0a 100644 --- a/packages/cli/src/commands/manifest.ts +++ b/packages/cli/src/commands/manifest.ts @@ -1,4 +1,4 @@ -import { Argument, Command, Program } from "./types"; +import { Argument, Command, Program, BaseCommandOptions } from "./types"; import { createLogger } from "./utils/createLogger"; import { defaultBuildManifest, @@ -69,23 +69,17 @@ const manifestTypes = [ "infra", "workflow", ] as const; -type ManifestType = typeof manifestTypes[number]; +export type ManifestType = typeof manifestTypes[number]; -type ManifestSchemaCommandOptions = { +export interface ManifestSchemaCommandOptions extends BaseCommandOptions { raw: boolean; - manifestFile: ManifestType; - verbose?: boolean; - quiet?: boolean; - logFile?: string; -}; + manifestFile: string | false; +} -type ManifestMigrateCommandOptions = { - manifestFile: string; - format: string; - verbose?: boolean; - quiet?: boolean; - logFile?: string; -}; +export interface ManifestMigrateCommandOptions extends BaseCommandOptions { + manifestFile: string | false; + format: string | false; +} export const manifest: Command = { setup: (program: Program) => { @@ -107,11 +101,7 @@ export const manifest: Command = { .choices(manifestTypes) .default(manifestTypes[0]) ) - .option( - `-r, --raw`, - intlMsg.commands_manifest_command_s_option_r(), - false - ) + .option(`-r, --raw`, intlMsg.commands_manifest_command_s_option_r()) .option( `-m, --manifest-file <${pathStr}>`, `${intlMsg.commands_manifest_options_m({ @@ -124,9 +114,12 @@ export const manifest: Command = { `-l, --log-file [${pathStr}]`, `${intlMsg.commands_build_options_l()}` ) - .action(async (type, options) => { + .action(async (type, options: Partial) => { await runSchemaCommand(type, { - ...options, + raw: options.raw || false, + manifestFile: options.manifestFile || false, + verbose: options.verbose || false, + quiet: options.quiet || false, logFile: parseLogFileOption(options.logFile), }); }); @@ -160,9 +153,12 @@ export const manifest: Command = { ) .option("-v, --verbose", intlMsg.commands_common_options_verbose()) .option("-q, --quiet", intlMsg.commands_common_options_quiet()) - .action(async (type, options) => { + .action(async (type, options: Partial) => { await runMigrateCommand(type, { - ...options, + manifestFile: options.manifestFile || false, + format: options.format || false, + verbose: options.verbose || false, + quiet: options.quiet || false, logFile: parseLogFileOption(options.logFile), }); }); @@ -171,7 +167,7 @@ export const manifest: Command = { export const runSchemaCommand = async ( type: ManifestType, - options: ManifestSchemaCommandOptions + options: Required ): Promise => { const { verbose, quiet, logFile } = options; const logger = createLogger({ verbose, quiet, logFile }); @@ -358,7 +354,7 @@ export const runSchemaCommand = async ( const runMigrateCommand = async ( type: ManifestType, - options: ManifestMigrateCommandOptions + options: Required ) => { const { verbose, quiet, logFile } = options; const logger = createLogger({ verbose, quiet, logFile }); @@ -393,7 +389,7 @@ const runMigrateCommand = async ( return migrateManifestFile( manifestFile, migratePolywrapProjectManifest, - options.format ?? latestPolywrapManifestFormat, + options.format || latestPolywrapManifestFormat, logger ); } else if (isAppManifestLanguage(language)) { @@ -405,7 +401,7 @@ const runMigrateCommand = async ( return migrateManifestFile( manifestFile, migrateAppProjectManifest, - options.format ?? latestAppManifestFormat, + options.format || latestAppManifestFormat, logger ); } else if (isPluginManifestLanguage(language)) { @@ -417,7 +413,7 @@ const runMigrateCommand = async ( return migrateManifestFile( manifestFile, migratePluginProjectManifest, - options.format ?? latestPluginManifestFormat, + options.format || latestPluginManifestFormat, logger ); } @@ -435,7 +431,7 @@ const runMigrateCommand = async ( migrateManifestFile( parseManifestFileOption(options.manifestFile, defaultBuildManifest), migrateBuildExtensionManifest, - options.format ?? latestBuildManifestFormat, + options.format || latestBuildManifestFormat, logger ); break; @@ -449,7 +445,7 @@ const runMigrateCommand = async ( migrateManifestFile( parseManifestFileOption(options.manifestFile, defaultDeployManifest), migrateDeployExtensionManifest, - options.format ?? latestDeployManifestFormat, + options.format || latestDeployManifestFormat, logger ); break; @@ -463,7 +459,7 @@ const runMigrateCommand = async ( migrateManifestFile( parseManifestFileOption(options.manifestFile, defaultInfraManifest), migrateInfraExtensionManifest, - options.format ?? latestInfraManifestFormat, + options.format || latestInfraManifestFormat, logger ); break; @@ -477,7 +473,7 @@ const runMigrateCommand = async ( migrateManifestFile( parseManifestFileOption(options.manifestFile, defaultWorkflowManifest), migrateWorkflow, - options.format ?? latestPolywrapWorkflowFormat, + options.format || latestPolywrapWorkflowFormat, logger ); break; @@ -552,7 +548,7 @@ function maybeFailOnUnsupportedManifestFormat( } function maybeFailOnUnsupportedTargetFormat( - format: string | undefined, + format: string | undefined | false, formats: string[], logger: Logger ) { diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index b66a402d00..842253561f 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -1,4 +1,4 @@ -import { Command, Program } from "./types"; +import { Command, Program, BaseCommandOptions } from "./types"; import { intlMsg, JobResult, @@ -23,20 +23,16 @@ import { createLogger } from "./utils/createLogger"; import path from "path"; import yaml from "yaml"; import fs from "fs"; -import { IClientConfigBuilder } from "@polywrap/client-config-builder-js"; -import { Env } from "@polywrap/core-js"; - -type WorkflowCommandOptions = { - configBuilder: IClientConfigBuilder; - wrapperEnvs: Env[]; - manifest: string; - jobs?: string[]; - validationScript?: string; - outputFile?: string; - verbose?: boolean; - quiet?: boolean; - logFile?: string; -}; +import { Env, Uri } from "@polywrap/core-js"; + +export interface TestCommandOptions extends BaseCommandOptions { + clientConfig: string | false; + wrapperEnvs: string | false; + manifestFile: string; + jobs: string[] | false; + validationScript: string | false; + outputFile: string | false; +} const defaultManifestStr = defaultWorkflowManifest.join(" | "); const pathStr = intlMsg.commands_test_options_m_path(); @@ -75,44 +71,61 @@ export const test: Command = { `-l, --log-file [${pathStr}]`, `${intlMsg.commands_build_options_l()}` ) - .action(async (options) => { + .action(async (options: Partial) => { await _run({ - ...options, - manifest: parseManifestFileOption( + manifestFile: parseManifestFileOption( options.manifestFile, defaultWorkflowManifest ), - configBuilder: await parseClientConfigOption(options.clientConfig), - wrapperEnvs: await parseWrapperEnvsOption(options.wrapperEnvs), + clientConfig: options.clientConfig || false, + wrapperEnvs: options.wrapperEnvs || false, outputFile: options.outputFile ? parseWorkflowOutputFilePathOption(options.outputFile) - : undefined, + : false, + jobs: options.jobs || false, + validationScript: options.validationScript || false, + verbose: options.verbose || false, + quiet: options.quiet || false, logFile: parseLogFileOption(options.logFile), }); }); }, }; -const _run = async (options: WorkflowCommandOptions) => { +const _run = async (options: Required) => { const { - manifest, - configBuilder, + manifestFile, + clientConfig, wrapperEnvs, outputFile, + jobs, verbose, quiet, - jobs, logFile, } = options; const logger = createLogger({ verbose, quiet, logFile }); - if (wrapperEnvs) { - configBuilder.addEnvs(wrapperEnvs); + const envs = await parseWrapperEnvsOption(wrapperEnvs); + const configBuilder = await parseClientConfigOption(clientConfig); + + if (envs) { + configBuilder.addEnvs(envs as Env[]); } - const manifestPath = path.resolve(manifest); + const manifestPath = path.resolve(manifestFile); const workflow = await loadWorkflowManifest(manifestPath, logger); + validateJobNames(workflow.jobs); + + const jobsArray: string[] = []; + if (jobs) { + jobs.forEach((x) => + jobsArray.push(...(x.includes(",") ? x.split(",") : [x])) + ); + } else { + jobsArray.push(...Object.keys(workflow.jobs)); + } + const validationScript = workflow.validation ? loadValidationScript(manifestPath, workflow.validation) : undefined; @@ -142,7 +155,7 @@ const _run = async (options: WorkflowCommandOptions) => { }; const jobRunner = new JobRunner(configBuilder, onExecution); - await jobRunner.run(workflow.jobs, jobs ?? Object.keys(workflow.jobs)); + await jobRunner.run(workflow.jobs, jobsArray); if (outputFile) { const outputFileExt = path.extname(outputFile).substring(1); diff --git a/packages/cli/src/commands/types.ts b/packages/cli/src/commands/types.ts index 03c1f5d622..3bff93fda0 100644 --- a/packages/cli/src/commands/types.ts +++ b/packages/cli/src/commands/types.ts @@ -6,3 +6,21 @@ export { Program, Argument }; export interface Command { setup: (program: Program) => MaybeAsync; } + +type SerializableOption = string | number | boolean; + +export interface BaseCommandOptions { + [prop: string]: SerializableOption | SerializableOption[]; + verbose: boolean; + quiet: boolean; + logFile: string | false; +} + +export interface CommandTypes { + options: BaseCommandOptions; + arguments: string[]; +} + +export type CommandTypeMapping = { + [name: string]: BaseCommandOptions | CommandTypes | CommandTypeMapping; +}; diff --git a/packages/cli/src/commands/utils/createLogger.ts b/packages/cli/src/commands/utils/createLogger.ts index 1bcf79841a..6375371fa6 100644 --- a/packages/cli/src/commands/utils/createLogger.ts +++ b/packages/cli/src/commands/utils/createLogger.ts @@ -5,7 +5,7 @@ import { LogLevel } from "@polywrap/logging-js"; export function createLogger(options: { verbose?: boolean; quiet?: boolean; - logFile?: string; + logFile?: string | false; }): Logger { const level = options.quiet ? LogLevel.ERROR diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000000..f3218a25c7 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,2 @@ +export * from "./commands"; +export * from "./lib"; diff --git a/packages/cli/src/lib/build-strategies/strategies/DockerImageStrategy.ts b/packages/cli/src/lib/build-strategies/strategies/DockerImageStrategy.ts index 4f134eb469..159a54de69 100644 --- a/packages/cli/src/lib/build-strategies/strategies/DockerImageStrategy.ts +++ b/packages/cli/src/lib/build-strategies/strategies/DockerImageStrategy.ts @@ -149,7 +149,8 @@ export class DockerImageBuildStrategy extends BuildStrategy { useBuildx &&= await this._isDockerBuildxInstalled(); const { stdout: containerLsOutput } = runCommandSync( - "docker container ls -a", + "docker", + ["container", "ls", "-a"], this.project.logger ); @@ -157,18 +158,24 @@ export class DockerImageBuildStrategy extends BuildStrategy { containerLsOutput && containerLsOutput.indexOf(`root-${imageName}`) > -1 ) { - await runCommand(`docker rm -f root-${imageName}`, this.project.logger); + await runCommand( + "docker", + ["rm", "-f", `root-${imageName}`], + this.project.logger + ); } // Create a new interactive terminal await runCommand( - `docker create -ti --name root-${imageName} ${imageName}`, + "docker", + ["create", "-ti", "--name", `root-${imageName}`, imageName], this.project.logger ); // Make sure the "project" directory exists const { stdout: projectLsOutput } = runCommandSync( - `docker run --rm ${imageName} /bin/bash -c "ls /project"`, + "docker", + ["run", "--rm", imageName, "/bin/bash", "-c", '"ls /project"'], this.project.logger ); @@ -179,7 +186,8 @@ export class DockerImageBuildStrategy extends BuildStrategy { } const { stdout: buildLsOutput } = runCommandSync( - `docker run --rm ${imageName} /bin/bash -c "ls /project/build"`, + "docker", + ["run", "--rm", imageName, "/bin/bash", "-c", '"ls /project/build"'], this.project.logger ); @@ -193,22 +201,28 @@ export class DockerImageBuildStrategy extends BuildStrategy { } await runCommand( - `docker cp root-${imageName}:/project/build/${buildArtifact} ${outputDir}`, + "docker", + ["cp", `root-${imageName}:/project/build/${buildArtifact}`, outputDir], this.project.logger ); - await runCommand(`docker rm -f root-${imageName}`, this.project.logger); + await runCommand( + "docker", + ["rm", "-f", `root-${imageName}`], + this.project.logger + ); if (useBuildx) { if (removeBuilder) { await runCommand( - `docker buildx rm ${imageName}`, + "docker", + ["buildx", "rm", imageName], this.project.logger ); } } if (removeImage) { - await runCommand(`docker rmi ${imageName}`, this.project.logger); + await runCommand("docker", ["rmi", imageName], this.project.logger); } }; @@ -250,7 +264,8 @@ export class DockerImageBuildStrategy extends BuildStrategy { let buildxUseFailed: boolean; try { const { stderr } = runCommandSync( - `docker buildx use ${imageName}`, + "docker", + ["buildx", "use", imageName], this.project.logger ); buildxUseFailed = !!stderr; @@ -260,12 +275,25 @@ export class DockerImageBuildStrategy extends BuildStrategy { if (buildxUseFailed) { await runCommand( - `docker buildx create --use --name ${imageName}`, + "docker", + ["buildx", "create", "--use", "--name", imageName], this.project.logger ); } await runCommand( - `docker buildx build -f ${dockerfile} -t ${imageName} ${rootDir} ${cacheFrom} ${cacheTo} --output=type=docker`, + "docker", + [ + "buildx", + "build", + "-f", + dockerfile, + "-t", + imageName, + rootDir, + cacheFrom, + cacheTo, + "--output=type=docker", + ], this.project.logger, undefined, undefined, @@ -273,7 +301,8 @@ export class DockerImageBuildStrategy extends BuildStrategy { ); } else { await runCommand( - `docker build -f ${dockerfile} -t ${imageName} ${rootDir}`, + "docker", + ["build", "-f", dockerfile, "-t", imageName, rootDir], this.project.logger, isWin() ? undefined @@ -288,7 +317,8 @@ export class DockerImageBuildStrategy extends BuildStrategy { // Get the docker image ID const { stdout } = runCommandSync( - `docker image inspect ${imageName} -f "{{.ID}}"`, + "docker", + ["image", "inspect", imageName, "-f", "{{.ID}}"], this.project.logger ); @@ -319,7 +349,8 @@ export class DockerImageBuildStrategy extends BuildStrategy { private async _isDockerBuildxInstalled(): Promise { const { stdout: version } = runCommandSync( - "docker buildx version", + "docker", + ["buildx", "version"], this.project.logger ); return version && version.startsWith("github.com/docker/buildx") diff --git a/packages/cli/src/lib/build-strategies/strategies/DockerVMStrategy.ts b/packages/cli/src/lib/build-strategies/strategies/DockerVMStrategy.ts index 2f17d440d7..779121b9c1 100644 --- a/packages/cli/src/lib/build-strategies/strategies/DockerVMStrategy.ts +++ b/packages/cli/src/lib/build-strategies/strategies/DockerVMStrategy.ts @@ -183,13 +183,22 @@ export class DockerVMBuildStrategy extends BuildStrategy { try { await runCommand( - `docker run --rm -v ${path.resolve( - this._volumePaths.project - )}:/project -v ${path.resolve( - this._volumePaths.linkedPackages - )}:/linked-packages ${cacheVolume} ${ - CONFIGS[language].baseImage - }:latest /bin/bash --verbose /project/polywrap-build.sh`, + "docker", + [ + "run", + "--rm", + "-v", + `${path.resolve(this._volumePaths.project)}:/project`, + "-v", + `${path.resolve( + this._volumePaths.linkedPackages + )}:/linked-packages`, + cacheVolume, + `${CONFIGS[language].baseImage}:latest`, + "/bin/bash", + "--verbose", + "/project/polywrap-build.sh", + ], this.project.logger, undefined, undefined, @@ -202,13 +211,21 @@ export class DockerVMBuildStrategy extends BuildStrategy { } await runCommand( - `docker run --rm -v ${path.resolve( - this._volumePaths.project - )}:/project -v ${path.resolve( - this._volumePaths.linkedPackages - )}:/linked-packages ${ - CONFIGS[language].baseImage - }:latest /bin/bash -c "chmod -R 777 /project && chmod -R 777 /linked-packages"`, + "docker", + [ + "run", + "--rm", + "-v", + `${path.resolve(this._volumePaths.project)}:/project`, + "-v", + `${path.resolve( + this._volumePaths.linkedPackages + )}:/linked-packages`, + `${CONFIGS[language].baseImage}:latest`, + "/bin/bash", + "-c", + '"chmod -R 777 /project && chmod -R 777 /linked-packages"', + ], this.project.logger ); } catch (e) { diff --git a/packages/cli/src/lib/build-strategies/strategies/LocalStrategy.ts b/packages/cli/src/lib/build-strategies/strategies/LocalStrategy.ts index 799587f48b..7813527a98 100644 --- a/packages/cli/src/lib/build-strategies/strategies/LocalStrategy.ts +++ b/packages/cli/src/lib/build-strategies/strategies/LocalStrategy.ts @@ -16,6 +16,7 @@ export class LocalBuildStrategy extends BuildStrategy { const buildManifestConfig = buildManifest.config as BuildManifestConfig; if (buildManifestConfig.polywrap_module) { + const polywrapModuleDir = buildManifestConfig.polywrap_module.dir; let scriptPath = `${__dirname}/../../defaults/build-strategies/${bindLanguage}/${this.getStrategyName()}/local.sh`; if (bindLanguage.startsWith("wasm")) { @@ -23,15 +24,27 @@ export class LocalBuildStrategy extends BuildStrategy { scriptPath = customScript ?? scriptPath; } - const command = `chmod +x ${scriptPath} && ${scriptPath} ${buildManifestConfig.polywrap_module.dir} ${this.outputDir}`; - await logActivity( this.project.logger, intlMsg.lib_helpers_buildText(), intlMsg.lib_helpers_buildError(), intlMsg.lib_helpers_buildWarning(), async (logger) => { - return await runCommand(command, logger, undefined, process.cwd()); + return await runCommand( + "chmod", + ["+x", scriptPath], + logger, + undefined, + process.cwd() + ).then(() => + runCommand( + scriptPath, + [polywrapModuleDir, this.outputDir], + logger, + undefined, + process.cwd() + ) + ); } ); } diff --git a/packages/cli/src/lib/helpers/workflow-validator.ts b/packages/cli/src/lib/helpers/workflow-validator.ts index fc5252990a..a57a98b4aa 100644 --- a/packages/cli/src/lib/helpers/workflow-validator.ts +++ b/packages/cli/src/lib/helpers/workflow-validator.ts @@ -10,7 +10,7 @@ import os from "os"; const TMPDIR = fs.mkdtempSync(path.join(os.tmpdir(), `polywrap-cli`)); export function cueExists(logger: Logger): boolean { - const { stdout } = runCommandSync("cue version", logger); + const { stdout } = runCommandSync("cue", ["version"], logger); return stdout ? stdout.startsWith("cue version ") : false; } @@ -51,8 +51,11 @@ export function validateOutput( JSON.stringify({ data, error: error?.message }, typesHandler, 2) ); - const args = [selector, validateScriptPath, jsonOutput]; - const { stderr } = runCommandSync(`cue vet -d ${args.join(" ")}`, logger); + const { stderr } = runCommandSync( + "cue", + ["vet", "-d", selector, validateScriptPath, jsonOutput], + logger + ); if (fs.existsSync(jsonOutput)) { fs.unlinkSync(jsonOutput); diff --git a/packages/cli/src/lib/index.ts b/packages/cli/src/lib/index.ts index 9be4c276bf..c68110b2cf 100644 --- a/packages/cli/src/lib/index.ts +++ b/packages/cli/src/lib/index.ts @@ -1,15 +1,17 @@ +export * from "./build-strategies"; +export * from "./codegen"; export * from "./deploy"; export * from "./helpers"; export * from "./infra"; export * from "./intl"; -export * from "./option-parsers"; +export * from "./logging"; +export * from "./manifest"; export * from "./option-defaults"; +export * from "./option-parsers"; export * from "./project"; export * from "./system"; export * from "./test-env"; +export * from "./workflow"; export * from "./CacheDirectory"; export * from "./Compiler"; -export * from "./codegen"; export * from "./SchemaComposer"; -export * from "./workflow"; -export * from "./logging"; diff --git a/packages/cli/src/lib/infra/fetchers/NodeDependencyFetcher.ts b/packages/cli/src/lib/infra/fetchers/NodeDependencyFetcher.ts index b33795c7db..c78df18fc7 100644 --- a/packages/cli/src/lib/infra/fetchers/NodeDependencyFetcher.ts +++ b/packages/cli/src/lib/infra/fetchers/NodeDependencyFetcher.ts @@ -10,8 +10,11 @@ export class NodeDependencyFetcher extends InfraDependencyFetcher { public async installPackages(packages: InfraPackageArg[]): Promise { this.composePackageJson(packages); await runCommand( - `cd ${this.config.installationDirectory} && npm i`, - this.config.logger + "npm", + ["i"], + this.config.logger, + undefined, + this.config.installationDirectory ); } @@ -48,8 +51,11 @@ export class YarnDependencyFetcher extends NodeDependencyFetcher { public async installPackages(packages: InfraPackageArg[]): Promise { this.composePackageJson(packages); await runCommand( - `cd ${this.config.installationDirectory} && yarn`, - this.config.logger + "yarn", + [], + this.config.logger, + undefined, + this.config.installationDirectory ); } } diff --git a/packages/cli/src/lib/option-parsers/client-config.ts b/packages/cli/src/lib/option-parsers/client-config.ts index ebe0b5b58f..6d57d580e8 100644 --- a/packages/cli/src/lib/option-parsers/client-config.ts +++ b/packages/cli/src/lib/option-parsers/client-config.ts @@ -7,7 +7,7 @@ import path from "path"; import { IClientConfigBuilder } from "@polywrap/client-config-builder-js"; export async function parseClientConfigOption( - clientConfig: string | undefined + clientConfig: string | undefined | false ): Promise { const builder = new ClientConfigBuilder().addDefaults(); diff --git a/packages/cli/src/lib/option-parsers/codegen.ts b/packages/cli/src/lib/option-parsers/codegen.ts index 4adf2c617a..07332b1a47 100644 --- a/packages/cli/src/lib/option-parsers/codegen.ts +++ b/packages/cli/src/lib/option-parsers/codegen.ts @@ -1,7 +1,7 @@ import path from "path"; export function parseCodegenScriptOption( - script: string | undefined -): string | undefined { - return script ? path.resolve(script) : undefined; + script: string | undefined | false +): string | false { + return script ? path.resolve(script) : false; } diff --git a/packages/cli/src/lib/option-parsers/dir.ts b/packages/cli/src/lib/option-parsers/dir.ts index c29d567380..beb51a42cb 100644 --- a/packages/cli/src/lib/option-parsers/dir.ts +++ b/packages/cli/src/lib/option-parsers/dir.ts @@ -1,7 +1,7 @@ import path from "path"; export function parseDirOption( - dir: string | undefined, + dir: string | undefined | false, defaultDir: string ): string { return dir ? path.resolve(dir) : path.resolve(defaultDir); diff --git a/packages/cli/src/lib/option-parsers/log-file.ts b/packages/cli/src/lib/option-parsers/log-file.ts index dcd5ef1511..e98949a001 100644 --- a/packages/cli/src/lib/option-parsers/log-file.ts +++ b/packages/cli/src/lib/option-parsers/log-file.ts @@ -2,7 +2,7 @@ import { getDefaultLogFileName } from "../option-defaults"; export function parseLogFileOption( logFile: string | boolean | undefined -): string | undefined { +): string | false { if (logFile) { if (logFile === true) { return getDefaultLogFileName(); @@ -10,5 +10,5 @@ export function parseLogFileOption( return logFile; } - return undefined; + return false; } diff --git a/packages/cli/src/lib/option-parsers/manifestFile.ts b/packages/cli/src/lib/option-parsers/manifestFile.ts index 00eceaa262..e744d9fc35 100644 --- a/packages/cli/src/lib/option-parsers/manifestFile.ts +++ b/packages/cli/src/lib/option-parsers/manifestFile.ts @@ -10,11 +10,10 @@ const deprecatedDefaultManifests = [ ]; export function parseManifestFileOption( - manifestFile: string | undefined, + manifestFile: string | undefined | false, defaults: string[] ): string { - const didUserProvideManifestFile = - manifestFile != undefined && !!manifestFile.length; + const didUserProvideManifestFile = manifestFile && !!manifestFile.length; const manifestPaths = manifestFile ? [manifestFile as string] : defaults; diff --git a/packages/cli/src/lib/option-parsers/wrapper-envs.ts b/packages/cli/src/lib/option-parsers/wrapper-envs.ts index 283ac10dde..f64c6fc08f 100644 --- a/packages/cli/src/lib/option-parsers/wrapper-envs.ts +++ b/packages/cli/src/lib/option-parsers/wrapper-envs.ts @@ -8,7 +8,7 @@ import YAML from "yaml"; type WrapperEnvs = Record>; export async function parseWrapperEnvsOption( - wrapperEnvsPath: string | undefined + wrapperEnvsPath: string | false | undefined ): Promise[]> | undefined> { if (!wrapperEnvsPath) { return undefined; diff --git a/packages/cli/src/lib/system/child-process.ts b/packages/cli/src/lib/system/child-process.ts index 1f67338071..1c86c0f855 100644 --- a/packages/cli/src/lib/system/child-process.ts +++ b/packages/cli/src/lib/system/child-process.ts @@ -1,16 +1,17 @@ import { Logger } from "../logging"; -import { exec, ExecException, execSync, SpawnSyncReturns } from "child_process"; +import { ExecException, SpawnSyncReturns, execSync, exec } from "child_process"; export function runCommandSync( command: string, + args: string[], logger: Logger, env: Record | undefined = undefined ): { stdout?: string; stderr?: SpawnSyncReturns & Error } { - logger.info(`> ${command}`); + logger.info(`> ${command} ${args.join(" ")}`); try { - const stdout = execSync(command, { + const stdout = execSync(`${command} ${args.join(" ")}`, { cwd: __dirname, env: { ...process.env, @@ -26,12 +27,13 @@ export function runCommandSync( export async function runCommand( command: string, + args: string[], logger: Logger, env: Record | undefined = undefined, cwd: string | undefined = undefined, redirectStderr = false ): Promise<{ stdout: string; stderr: string }> { - logger.info(`> ${command}`); + logger.info(`> ${command} ${args.join(" ")}`); return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { const callback = ( @@ -47,7 +49,7 @@ export async function runCommand( }; const childObj = exec( - command, + `${command} ${args.join(" ")}`, { cwd: cwd ?? __dirname, env: { diff --git a/packages/cli/src/lib/system/docker.ts b/packages/cli/src/lib/system/docker.ts index 46aac1aff5..ec599aa427 100644 --- a/packages/cli/src/lib/system/docker.ts +++ b/packages/cli/src/lib/system/docker.ts @@ -8,13 +8,13 @@ import path from "path"; import fs from "fs"; export function isDockerInstalled(logger: Logger): boolean { - const { stdout } = runCommandSync("docker version", logger); + const { stdout } = runCommandSync("docker", ["version"], logger); return stdout ? stdout.includes("Version") : false; } export async function ensureDockerDaemonRunning(logger: Logger): Promise { try { - runCommandSync("docker stats --no-stream", logger); + runCommandSync("docker", ["stats", "--no-stream"], logger); } catch (e) { throw new Error(intlMsg.lib_helpers_docker_couldNotConnect()); } diff --git a/packages/js/cli/README.md b/packages/js/cli/README.md new file mode 100644 index 0000000000..34e66ace2b --- /dev/null +++ b/packages/js/cli/README.md @@ -0,0 +1,31 @@ +# @polywrap/cli-js + +Programmatically execute the Polywrap CLI + +# Description + +It allows user execute Polywrap CLI commands programmatically, with simple and type-safe methods. + +# Usage + +Build a wrapper: +``` typescript +import { Commands } from "@polywrap/cli-js"; + +async function main() { + const wrapperPath = "/path/to/wrapper"; + + const res = await Commands.build( + { }, // build command options + { cwd: wrapperPath } + ); + + console.log(res.stdout); + console.log(res.stderr); + console.log(res.exitCode); +} +``` + +# Reference + +TODO diff --git a/packages/js/cli/jest.config.js b/packages/js/cli/jest.config.js new file mode 100644 index 0000000000..a5ca876a5a --- /dev/null +++ b/packages/js/cli/jest.config.js @@ -0,0 +1,11 @@ +module.exports = { + collectCoverage: true, + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"], + globals: { + 'ts-jest': { + diagnostics: false + } + } +}; diff --git a/packages/js/cli/package.json b/packages/js/cli/package.json new file mode 100644 index 0000000000..9a7d1a1498 --- /dev/null +++ b/packages/js/cli/package.json @@ -0,0 +1,34 @@ +{ + "name": "@polywrap/cli-js", + "description": "Programmatically execute the Polywrap CLI", + "version": "0.10.0-pre.6", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/polywrap/toolchain.git" + }, + "main": "build/index.js", + "files": [ + "build" + ], + "scripts": { + "build": "rimraf ./build && tsc --project tsconfig.build.json", + "lint": "eslint --color -c ../../../.eslintrc.js src/", + "test": "jest --passWithNoTests --runInBand --verbose", + "test:ci": "jest --passWithNoTests --runInBand --detectOpenHandles --verbose" + }, + "dependencies": { + "polywrap": "0.10.0-pre.6", + "spawn-command": "0.0.2-1" + }, + "devDependencies": { + "@types/jest": "26.0.8", + "jest": "26.6.3", + "rimraf": "3.0.2", + "ts-jest": "26.5.4", + "typescript": "4.1.6" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/js/cli/src/__tests__/commands.spec.ts b/packages/js/cli/src/__tests__/commands.spec.ts new file mode 100644 index 0000000000..56edc81552 --- /dev/null +++ b/packages/js/cli/src/__tests__/commands.spec.ts @@ -0,0 +1,296 @@ +import { Commands, CliConfig } from "../../"; + +import { + CommandTypes, + CommandTypings, + CommandTypeMapping, + BaseCommandOptions, +} from "polywrap"; +import { + initTestEnvironment, + stopTestEnvironment, + ensAddresses, + providers +} from "@polywrap/test-env-js"; +import { GetPathToCliTestFiles } from "@polywrap/test-cases"; +import fs from "fs"; +import os from "os"; +import path from "path"; + +jest.setTimeout(300_000); + +type CommandTestCase = CliConfig & { + options?: Partial; + before?: (test: CommandTestCase) => Promise | void; + after: ( + test: CommandTestCase, + stdout: string, + stderr: string, + exitCode: number + ) => Promise | void; +}; + +type CommandTestCases = CommandTestCase[]; + +type CommandTestCasesWithArgs = + (CommandTestCase & + { arguments: TArgs; })[]; + +type CommandTestCaseData< + TCommands +> = Required<{ + [Command in keyof TCommands]: + TCommands[Command] extends BaseCommandOptions ? + CommandTestCases : + TCommands[Command] extends CommandTypes ? + CommandTestCasesWithArgs : + TCommands[Command] extends CommandTypeMapping ? + CommandTestCaseData : never; +}>; + +const clearDir = (dir: string) => { + if (fs.existsSync(dir)) { + fs.rmSync(dir, { recursive: true }); + } + expect(fs.existsSync(dir)).toBeFalsy(); +} + +const testData: CommandTestCaseData = { + build: [{ + options: { strategy: "vm", codegen: true }, + cwd: path.join(GetPathToCliTestFiles(), "wasm/build-cmd/assemblyscript/001-sanity"), + before: async (test) => { + // clear build dir + if (!test.cwd) throw Error("This shouldn't happen"); + clearDir(path.join(test.cwd, "build")); + }, + after: (test) => { + // check for build dir and artifacts + if (!test.cwd) throw Error("This shouldn't happen"); + const buildDir = path.join(test.cwd, "build"); + const wasmPath = path.join(buildDir, "wrap.wasm"); + const manifestPath = path.join(buildDir, "wrap.info"); + expect(fs.existsSync(buildDir)).toBeTruthy(); + expect(fs.existsSync(wasmPath)).toBeTruthy(); + expect(fs.existsSync(manifestPath)).toBeTruthy(); + } + }], + codegen: [{ + options: { codegenDir: "./test" }, + cwd: path.join(GetPathToCliTestFiles(), "wasm/codegen/001-sanity-assemblyscript"), + before: (test) => { + // clear build dir + if (!test.cwd || !test.options?.codegenDir) + throw Error("This shouldn't happen"); + const outputDir = path.join(test.cwd, test.options?.codegenDir); + clearDir(outputDir); + }, + after: (test) => { + // check for build dir and artifacts + if (!test.cwd || !test.options?.codegenDir) + throw Error("This shouldn't happen"); + const outputDir = path.join(test.cwd, test.options?.codegenDir); + expect(fs.existsSync(outputDir)).toBeTruthy(); + clearDir(outputDir); + } + }], + create: { + app: [{ + cwd: fs.mkdtempSync(path.join(os.tmpdir(), "cli-js-create-test")), + arguments: ["typescript-node", "test-app"], + after: (test) => { + if (!test.cwd) + throw Error("This shouldn't happen"); + const outputDir = path.join(test.cwd, "test-app"); + const packagePath = path.join(outputDir, "package.json"); + expect(fs.existsSync(outputDir)).toBeTruthy(); + expect(fs.existsSync(packagePath)).toBeTruthy(); + clearDir(test.cwd); + } + }], + plugin: [{ + cwd: fs.mkdtempSync(path.join(os.tmpdir(), "cli-js-create-test")), + arguments: ["typescript", "test-plugin"], + after: (test) => { + if (!test.cwd) + throw Error("This shouldn't happen"); + const outputDir = path.join(test.cwd, "test-plugin"); + const packagePath = path.join(outputDir, "package.json"); + expect(fs.existsSync(outputDir)).toBeTruthy(); + expect(fs.existsSync(packagePath)).toBeTruthy(); + clearDir(test.cwd); + } + }], + wasm: [{ + cwd: fs.mkdtempSync(path.join(os.tmpdir(), "cli-js-create-test")), + arguments: ["rust", "test-wasm"], + after: (test, stdout, stderr, exitCode) => { + if (!test.cwd) + throw Error("This shouldn't happen"); + const outputDir = path.join(test.cwd, "test-wasm"); + const packagePath = path.join(outputDir, "Cargo.toml"); + expect(fs.existsSync(outputDir)).toBeTruthy(); + expect(fs.existsSync(packagePath)).toBeTruthy(); + clearDir(test.cwd); + } + }] + }, + deploy: [{ + cwd: path.join(GetPathToCliTestFiles(), "wasm/deploy/001-sanity"), + env: { + PATH: process.env.PATH || "", + IPFS_GATEWAY_URI: providers.ipfs, + DOMAIN_NAME: "test1.eth", + ENS_REG_ADDR: ensAddresses.ensAddress, + ENS_REGISTRAR_ADDR: ensAddresses.registrarAddress, + ENS_RESOLVER_ADDR: ensAddresses.resolverAddress, + }, + before: async () => { + await stopTestEnvironment(); + await initTestEnvironment(); + + // Wait a little longer just in case + await new Promise((resolve) => setTimeout(resolve, 3000)); + }, + after: async (_, stdout) => { + expect(stdout).toContain( + "Successfully executed" + ); + await stopTestEnvironment(); + } + }], + docgen: [{ + cwd: path.join(GetPathToCliTestFiles(), "docgen", "001-sanity"), + arguments: ["docusaurus"], + after: (_, stdout) => { + expect(stdout).toContain("Docs were generated successfully"); + } + }], + infra: [{ + cwd: path.join(GetPathToCliTestFiles(), "infra/001-sanity"), + env: { + PATH: process.env.PATH || "", + ENV_IPFS_PORT: "5001", + }, + arguments: ["config"], + after: (_, stdout) => { + expect(stdout).toContain("services:"); + } + }], + manifest: { + migrate: [{ + cwd: fs.mkdtempSync(path.join(os.tmpdir(), "manifest-migrate")), + arguments: ["project"], + before: (test) => { + if (!test.cwd) + throw Error("This shouldn't happen"); + fs.copyFileSync( + path.join(GetPathToCliTestFiles(), "manifest/samples/polywrap.yaml"), + path.join(test.cwd, "polywrap.yaml") + ); + }, + after: (_, stdout, __, exitCode) => { + expect(stdout).toContain("Migrating polywrap.yaml to version"); + expect(exitCode).toBe(0); + } + }], + schema: [{ + cwd: path.join(GetPathToCliTestFiles(), "manifest/samples"), + arguments: ["build"], + after: (_, stdout) => { + expect(stdout).toContain("format: "); + } + }] + }, + test: [{ + cwd: path.join(GetPathToCliTestFiles(), "test/001-yaml-workflow"), + before: async (test) => { + if (!test.cwd) + throw Error("This shouldn't happen"); + const wrapperPath = path.join(test.cwd, "../run-test-wrapper"); + await Commands.build({ codegen: true }, { cwd: wrapperPath }); + }, + after: (_, stdout, __, exitCode) => { + expect(stdout).toContain("Data: "); + expect(exitCode).toBe(0); + } + }] +}; + +describe("Commands", () => { + const resolvePropPath = (props: string[], obj: any) => { + let resolved = obj; + for (const prop of props) { + if (!resolved) { + return undefined; + } + resolved = resolved[prop]; + } + return resolved; + } + + const runCommandTests = (props: string[]) => { + const command = resolvePropPath(props, Commands); + + if (!command) { + throw Error(`Invalid command path: ${props.join(".")}`); + } + + if ( + typeof command !== "function" && + typeof command === "object" + ) { + for (const prop of Object.keys(command)) { + runCommandTests([...props, prop]); + } + } + + describe(props.join("."), () => { + const tests = + resolvePropPath(props, testData) as + CommandTestCasesWithArgs; + + if (!tests) { + throw Error(`Test data for Commands.${props.join(".")} not defined.`); + } + + for (let i = 0; i < tests.length; ++i) { + let test = tests[i]; + + it(`test #${i}`, async () => { + let result: { + exitCode: number; + stdout: string; + stderr: string; + }; + const cliConfig = { + cwd: test.cwd, + cli: test.cli, + env: test.env + }; + + if (test.before) { + await test.before(test); + } + + if (test.arguments) { + result = await command(...test.arguments, test.options, cliConfig); + } else { + result = await command(test.options, cliConfig); + } + + await test.after( + test, + result.stdout, + result.stderr, + result.exitCode + ); + }); + } + }); + } + + for (const command of Object.keys(Commands)) { + runCommandTests([command]); + } +}); diff --git a/packages/js/cli/src/commands/exec.ts b/packages/js/cli/src/commands/exec.ts new file mode 100644 index 0000000000..baae2fc309 --- /dev/null +++ b/packages/js/cli/src/commands/exec.ts @@ -0,0 +1,109 @@ +import { runCli, CliConfig } from "../run-cli"; + +import { BaseCommandOptions } from "polywrap"; + +export type CommandFn = ( + options?: Partial, + config?: CliConfig +) => ReturnType; + +export type CommandWithArgsFn< + TArguments extends unknown[], + TOptions extends BaseCommandOptions +> = ( + ...args: [ + ...targs: TArguments, + options?: Partial, + config?: CliConfig + ] +) => ReturnType; + +export function execCommandFn( + command: string +): CommandFn { + return async (options?: Partial, config?: CliConfig) => { + const parsedArgs = [...command.split(" "), ...parseOptions(options)]; + return await runCli({ + args: parsedArgs, + config, + }); + }; +} + +export function execCommandWithArgsFn< + TTypes extends { + options: BaseCommandOptions; + arguments: unknown[]; + }, + TArguments extends unknown[] = TTypes["arguments"], + TOptions extends BaseCommandOptions = TTypes["options"] +>(command: string): CommandWithArgsFn { + return async ( + ...args: [ + ...targs: TArguments, + options?: Partial, + config?: CliConfig + ] + ) => { + const commandArgs = []; + let options = {}; + let optionsFound = false; + let config: CliConfig | undefined = {}; + + // Iterate through the variadic arguments + for (const arg of args) { + if (!optionsFound) { + if (typeof arg === "string") { + commandArgs.push(arg); + } else if (typeof arg === "object") { + options = arg as Record; + optionsFound = true; + } else if (typeof arg === "undefined") { + // undefined options + optionsFound = true; + } + } else { + if (typeof arg !== "object" && typeof arg !== "undefined") { + throw new Error(`Invalid CliConfig argument type: ${arg}`); + } + config = arg as CliConfig | undefined; + break; + } + } + + const parsedArgs = [ + ...command.split(" "), + ...commandArgs, + ...parseOptions(options), + ]; + return await runCli({ + args: parsedArgs, + config, + }); + }; +} + +function toKebabCase(camelCase: string): string { + return camelCase.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); +} + +function parseValue(value: string | string[] | boolean): string { + if (Array.isArray(value)) { + return value.join(" "); + } + return value.toString(); +} + +function parseOptions( + options?: Partial +): string[] { + const parsed: string[] = []; + if (options) { + for (const [key, value] of Object.entries(options)) { + if (value === undefined) continue; + parsed.push(`--${toKebabCase(key)}`); + parsed.push(parseValue(value)); + } + } + return parsed; +} diff --git a/packages/js/cli/src/commands/index.ts b/packages/js/cli/src/commands/index.ts new file mode 100644 index 0000000000..06ff32f5de --- /dev/null +++ b/packages/js/cli/src/commands/index.ts @@ -0,0 +1,54 @@ +import { + CommandFn, + CommandWithArgsFn, + execCommandFn, + execCommandWithArgsFn, +} from "./exec"; + +import { + CommandTypes, + CommandTypings, + CommandTypeMapping, + BaseCommandOptions, +} from "polywrap"; + +type CommandFns = Required< + { + [Command in keyof TCommands]: TCommands[Command] extends BaseCommandOptions + ? CommandFn + : TCommands[Command] extends CommandTypes + ? CommandWithArgsFn< + TCommands[Command]["arguments"], + TCommands[Command]["options"] + > + : TCommands[Command] extends CommandTypeMapping + ? CommandFns + : never; + } +>; + +export const commands: CommandFns = { + build: execCommandFn("build"), + codegen: execCommandFn("codegen"), + create: { + app: execCommandWithArgsFn("create app"), + plugin: execCommandWithArgsFn( + "create plugin" + ), + wasm: execCommandWithArgsFn( + "create wasm" + ), + }, + deploy: execCommandFn("deploy"), + docgen: execCommandWithArgsFn("docgen"), + infra: execCommandWithArgsFn("infra"), + manifest: { + migrate: execCommandWithArgsFn( + "manifest migrate" + ), + schema: execCommandWithArgsFn( + "manifest schema" + ), + }, + test: execCommandFn("test"), +}; diff --git a/packages/js/cli/src/declarations.d.ts b/packages/js/cli/src/declarations.d.ts new file mode 100644 index 0000000000..37d190a635 --- /dev/null +++ b/packages/js/cli/src/declarations.d.ts @@ -0,0 +1,7 @@ +declare module "spawn-command" { + import { ChildProcess } from "child_process"; + export default function spawn( + command: string, + options?: Record + ): ChildProcess; +} diff --git a/packages/js/cli/src/index.ts b/packages/js/cli/src/index.ts new file mode 100644 index 0000000000..7765a894e0 --- /dev/null +++ b/packages/js/cli/src/index.ts @@ -0,0 +1,2 @@ +export { commands as Commands } from "./commands"; +export * from "./run-cli"; diff --git a/packages/js/cli/src/run-cli.ts b/packages/js/cli/src/run-cli.ts new file mode 100644 index 0000000000..4ee3d55232 --- /dev/null +++ b/packages/js/cli/src/run-cli.ts @@ -0,0 +1,65 @@ +import path from "path"; +import fs from "fs"; +import spawn from "spawn-command"; + +const monorepoCli = `${__dirname}/../../../cli/bin/polywrap`; +const npmCli = `${__dirname}/../../../polywrap/bin/polywrap`; + +export interface CliConfig { + cwd?: string; + cli?: string; + env?: Record; +} + +export const runCli = async (options: { + args: string[]; + config?: CliConfig; +}): Promise<{ + exitCode: number; + stdout: string; + stderr: string; +}> => { + const config: CliConfig = options.config || {}; + const args = options.args; + + return new Promise((resolve, reject) => { + if (!config.cwd) { + // Make sure to set an absolute working directory + const cwd = process.cwd(); + config.cwd = cwd[0] !== "/" ? path.resolve(__dirname, cwd) : cwd; + } + + // Resolve the CLI + if (!config.cli) { + if (fs.existsSync(monorepoCli)) { + config.cli = monorepoCli; + } else if (fs.existsSync(npmCli)) { + config.cli = npmCli; + } else { + throw Error(`runCli is missing a valid CLI path, please provide one`); + } + } + + const command = `node ${config.cli} ${args.join(" ")}`; + const child = spawn(command, { cwd: config.cwd, env: config.env }); + + let stdout = ""; + let stderr = ""; + + child.on("error", (error: Error) => { + reject(error); + }); + + child.stdout?.on("data", (data: string) => { + stdout += data.toString(); + }); + + child.stderr?.on("data", (data: string) => { + stderr += data.toString(); + }); + + child.on("exit", (exitCode: number) => { + resolve({ exitCode, stdout, stderr }); + }); + }); +}; diff --git a/packages/js/cli/tsconfig.build.json b/packages/js/cli/tsconfig.build.json new file mode 100644 index 0000000000..77aadfdd2f --- /dev/null +++ b/packages/js/cli/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "./src/**/*.ts" + ], + "exclude": [ + "./src/**/__tests__" + ] +} diff --git a/packages/js/cli/tsconfig.json b/packages/js/cli/tsconfig.json new file mode 100644 index 0000000000..d34dfd8f5a --- /dev/null +++ b/packages/js/cli/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig", + "compilerOptions": { + "outDir": "build" + }, + "include": [ + "./src/**/*.ts" + ], +} diff --git a/packages/js/client/src/__tests__/core/sanity.spec.ts b/packages/js/client/src/__tests__/core/sanity.spec.ts index 143a5455d8..88e9e9c956 100644 --- a/packages/js/client/src/__tests__/core/sanity.spec.ts +++ b/packages/js/client/src/__tests__/core/sanity.spec.ts @@ -11,7 +11,7 @@ import { buildWrapper } from "@polywrap/test-env-js"; import { ResultErr } from "@polywrap/result"; import { StaticResolver, UriResolverLike } from "@polywrap/uri-resolvers-js"; import { WasmPackage } from "@polywrap/wasm-js"; -import { defaultWrappers } from "@polywrap/client-config-builder-js"; +import { defaultInterfaces } from "@polywrap/client-config-builder-js"; jest.setTimeout(200000); @@ -36,7 +36,7 @@ describe("sanity", () => { implementations: [new Uri("wrap://plugin/logger")], }, { - interface: new Uri(defaultWrappers.concurrentInterface), + interface: new Uri(defaultInterfaces.concurrent), implementations: [new Uri("wrap://plugin/concurrent")], }, ]);