From 233935dbe0b28edec909730f4f0523a2fe423a3d Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Wed, 11 May 2022 21:15:32 -0700 Subject: [PATCH] tests: Add tons of integration tests. (#88) * Remove prop. * Move to int tests. * Add init tests. * Start testing node. * Update snap. * Use make in CI. * Check home dir. * Update snap. * Handle more node failures. * Test node types. * Move code to path utils. * Add node 18. * Start on system. * Start on system.ls * Log deps install. * Test pass through args. * Test scopes. * Test deps. * new: Wrap process utils into a `Command` struct. (#89) * First pass. * Second pass. * Fix npm install. * Add more fixtures. * Use a sandbox approach. * Fix race conditions. * Test more node stuff. * Always use sandbox. * Test install. * Standardize paths in logs. * Add some windows conditions. * Test syncing. * Fix workspace race conditions for root `tsconfig.json` (#90) * Start on things. * Use write. * Handle dirty. * Fix tests. * Standardize paths for tsconfig. * Test env vars. * Test cwd. * Test retry count. * Test caching. * Update cargo deps. * Fix teardown. * Add internet connection detection. * Allow input for commands. * Use modified time for svn. * Add local files to hashing. * Only load root package.json/tsconfig.json once (#91) * Move root code around. * Refactor install node deps. * Refactor tsconfig. * Fix issues. * Fix workspace. * Copy config. * Fix windows tests. * Test windows batch files. * And early aborting. * Drop node 12. * Fix linux tests. * Bump. * Add quiet back. * Better handling of failures. * Reduce logging. * Fix windows tests. * Enable logging. * Disable fail fast. * Test specific tests. * Add logging. * Stream output. * More logging. * Clean up pm tools. * Log vars. * Stuff. * new: Split toolchain into many traits. (#92) * Start splitting apart traits. * Rework tool/node. * Add npm and yarn. * Better npm global handling. * Clean up impls. * More trait work. * Add is_executable. * Try using generics. * Add generics and fix package managers. * Polish and fixes. * Test fixes. * Polish. * CI fixes. * Improve process logging. * Enable debugging. * Undo testing. --- .eslintignore | 5 +- .github/workflows/moon.yml | 3 +- .github/workflows/rust.yml | 26 +- .gitignore | 6 + .yarn/versions/5180ba6c.yml | 2 + Cargo.lock | 141 ++- Makefile.toml | 29 +- README.md | 18 + crates/cache/src/hasher.rs | 6 +- crates/cli/Cargo.toml | 2 + crates/cli/src/commands/bin.rs | 96 +- crates/cli/src/commands/ci.rs | 10 +- crates/cli/src/commands/init.rs | 8 +- crates/cli/src/commands/project.rs | 99 -- crates/cli/src/commands/project_graph.rs | 43 - crates/cli/src/commands/run.rs | 8 +- crates/cli/src/commands/setup.rs | 21 +- crates/cli/src/commands/teardown.rs | 14 +- crates/cli/src/helpers.rs | 21 - crates/cli/src/lib.rs | 1 - crates/cli/tests/bin_test.rs | 52 + crates/cli/tests/init_test.rs | 164 +++ crates/cli/tests/project_graph_test.rs | 38 + crates/cli/tests/project_test.rs | 95 ++ crates/cli/tests/run_test.rs | 1048 +++++++++++++++++ crates/cli/tests/setup_teardown_test.rs | 39 + .../project_graph_test__many_projects.snap} | 5 +- .../project_graph_test__no_projects.snap} | 5 +- ...test__single_project_no_dependencies.snap} | 5 +- ...st__single_project_with_dependencies.snap} | 5 +- .../project_test__advanced_config.snap} | 4 +- .../project_test__basic_config.snap} | 4 +- .../project_test__depends_on_paths.snap} | 4 +- .../project_test__empty_config.snap} | 4 +- .../snapshots/project_test__no_config.snap} | 4 +- .../project_test__unknown_project.snap} | 5 +- .../snapshots/project_test__with_tasks.snap} | 4 +- ...hing__uses_cache_on_subsequent_runs-2.snap | 15 + ...aching__uses_cache_on_subsequent_runs.snap | 13 + ...dependencies__runs_the_graph_in_order.snap | 18 + ...runs_the_graph_in_order_not_from_head.snap | 15 + ...n_test__errors_for_cycle_in_task_deps.snap | 11 + .../run_test__errors_for_unknown_project.snap | 9 + ...t__errors_for_unknown_task_in_project.snap | 9 + ...ode__engines__adds_engines_constraint.snap | 16 + ...ngines__doesnt_add_engines_constraint.snap | 13 + ...de__handles_process_exit_code_nonzero.snap | 14 + ..._node__handles_process_exit_code_zero.snap | 14 + ...t__node__handles_process_exit_nonzero.snap | 13 + ...test__node__handles_process_exit_zero.snap | 13 + ...test__node__handles_unhandled_promise.snap | 20 + ...un_test__node__inherits_moon_env_vars.snap | 21 + .../run_test__node__passes_args_through.snap | 12 + .../run_test__node__runs_cjs_files.snap | 13 + ...un_test__node__runs_from_project_root.snap | 12 + ..._test__node__runs_from_workspace_root.snap | 12 + .../run_test__node__runs_mjs_files.snap | 13 + ...run_test__node__runs_package_managers.snap | 12 + .../run_test__node__runs_standard_script.snap | 13 + .../run_test__node__sets_env_vars.snap | 14 + ..._test__node__supports_top_level_await.snap | 14 + ...__syncs_as_dependency_to_package_json.snap | 14 + ...syncs_as_reference_to_tsconfig_json-2.snap | 19 + ...__syncs_as_reference_to_tsconfig_json.snap | 22 + ...ion_manager__errors_for_invalid_value.snap | 13 + ...t__node_npm__installs_correct_version.snap | 12 + ...__node_pnpm__installs_correct_version.snap | 12 + ..._node_yarn1__installs_correct_version.snap | 12 + .../run_test__system__handles_echo.snap | 12 + .../run_test__system__handles_ls.snap | 19 + ...un_test__system__handles_ls_from_root.snap | 25 + ..._system__handles_process_exit_nonzero.snap | 13 + ...st__system__handles_process_exit_zero.snap | 13 + ..._test__system__inherits_moon_env_vars.snap | 21 + ...run_test__system__passes_args_through.snap | 12 + ...system__retries_on_failure_till_count.snap | 25 + .../run_test__system__runs_bash_script.snap | 13 + ..._test__system__runs_from_project_root.snap | 12 + ...est__system__runs_from_workspace_root.snap | 12 + .../run_test__system__sets_env_vars.snap | 14 + ...windows__handles_process_exit_nonzero.snap | 12 + ...em_windows__handles_process_exit_zero.snap | 12 + ...ystem_windows__inherits_moon_env_vars.snap | 12 + ...__system_windows__passes_args_through.snap | 11 + ...indows__retries_on_failure_till_count.snap | 24 + ...test__system_windows__runs_bat_script.snap | 12 + ...ystem_windows__runs_from_project_root.snap | 11 + ...tem_windows__runs_from_workspace_root.snap | 11 + ...n_test__system_windows__sets_env_vars.snap | 13 + ..._target_scopes__errors_for_deps_scope.snap | 9 + ..._target_scopes__errors_for_self_scope.snap | 9 + crates/config/src/lib.rs | 4 +- crates/config/src/package.rs | 5 +- crates/config/src/project/task.rs | 2 +- crates/config/src/tsconfig.rs | 13 +- crates/config/src/workspace/mod.rs | 2 +- crates/config/src/workspace/node.rs | 4 +- crates/config/templates/workspace.yml | 4 +- crates/error/src/lib.rs | 9 +- crates/logger/Cargo.toml | 1 + crates/logger/src/color.rs | 26 +- crates/logger/src/lib.rs | 5 + crates/project/src/file_group.rs | 2 +- crates/project/src/project.rs | 4 +- crates/project/src/task.rs | 12 +- crates/project/src/token.rs | 2 +- crates/project/tests/project_test.rs | 17 +- crates/toolchain/src/errors.rs | 3 + crates/toolchain/src/helpers.rs | 35 +- crates/toolchain/src/lib.rs | 7 +- crates/toolchain/src/pms/mod.rs | 3 + crates/toolchain/src/pms/npm.rs | 272 +++++ crates/toolchain/src/pms/pnpm.rs | 203 ++++ crates/toolchain/src/pms/yarn.rs | 279 +++++ crates/toolchain/src/tool.rs | 70 -- crates/toolchain/src/toolchain.rs | 193 +-- crates/toolchain/src/tools/mod.rs | 3 - crates/toolchain/src/tools/node.rs | 284 +++-- crates/toolchain/src/tools/npm.rs | 217 ---- crates/toolchain/src/tools/pnpm.rs | 194 --- crates/toolchain/src/tools/yarn.rs | 266 ----- crates/toolchain/src/traits.rs | 266 +++++ crates/toolchain/tests/node_test.rs | 51 +- crates/toolchain/tests/npm_test.rs | 26 +- crates/toolchain/tests/pnpm_test.rs | 26 +- crates/toolchain/tests/toolchain_test.rs | 16 +- crates/toolchain/tests/yarn_test.rs | 26 +- crates/utils/Cargo.toml | 4 + crates/utils/src/fs.rs | 137 +-- crates/utils/src/lib.rs | 18 + crates/utils/src/path.rs | 148 +++ crates/utils/src/process.rs | 436 ++++--- crates/utils/src/test.rs | 59 +- crates/workspace/src/action.rs | 29 +- crates/workspace/src/action_runner.rs | 96 +- .../workspace/src/actions/hashing/target.rs | 35 +- .../src/actions/install_node_deps.rs | 165 ++- crates/workspace/src/actions/run_target.rs | 211 ++-- .../workspace/src/actions/setup_toolchain.rs | 11 +- crates/workspace/src/actions/sync_project.rs | 174 +-- crates/workspace/src/dep_graph.rs | 17 +- crates/workspace/src/errors.rs | 9 +- crates/workspace/src/vcs/git.rs | 75 +- crates/workspace/src/vcs/mod.rs | 8 +- crates/workspace/src/vcs/svn.rs | 72 +- crates/workspace/src/workspace.rs | 106 +- docs/roadmap.md | 14 +- docs/workspace.md | 2 +- packages/runtime/src/context.ts | 2 +- schemas/workspace.json | 4 +- tests/fixtures/base/.moon/workspace.yml | 4 +- tests/fixtures/base/package.json | 5 +- tests/fixtures/cases/.moon/workspace.yml | 28 + tests/fixtures/cases/base/project.yml | 1 + tests/fixtures/cases/depends-on/package.json | 6 + tests/fixtures/cases/depends-on/project.yml | 9 + tests/fixtures/cases/depends-on/tsconfig.json | 3 + tests/fixtures/cases/deps-a/package.json | 3 + tests/fixtures/cases/deps-a/project.yml | 16 + tests/fixtures/cases/deps-b/package.json | 3 + tests/fixtures/cases/deps-b/project.yml | 16 + tests/fixtures/cases/deps-b/tsconfig.json | 3 + tests/fixtures/cases/deps-c/project.yml | 14 + tests/fixtures/cases/deps-c/tsconfig.json | 3 + tests/fixtures/cases/node/cjsFile.cjs | 1 + tests/fixtures/cases/node/cwd.js | 1 + tests/fixtures/cases/node/envVars.js | 5 + tests/fixtures/cases/node/envVarsMoon.js | 5 + tests/fixtures/cases/node/exitCodeNonZero.js | 6 + tests/fixtures/cases/node/exitCodeZero.js | 6 + tests/fixtures/cases/node/mjsFile.mjs | 1 + tests/fixtures/cases/node/passthroughArgs.js | 1 + .../fixtures/cases/node/processExitNonZero.js | 6 + tests/fixtures/cases/node/processExitZero.js | 6 + tests/fixtures/cases/node/project.yml | 60 + tests/fixtures/cases/node/standard.js | 2 + tests/fixtures/cases/node/throwError.js | 4 + tests/fixtures/cases/node/topLevelAwait.mjs | 10 + tests/fixtures/cases/node/unhandledPromise.js | 6 + tests/fixtures/cases/package.json | 7 + tests/fixtures/cases/system-windows/cwd.bat | 1 + .../fixtures/cases/system-windows/envVars.bat | 3 + .../cases/system-windows/envVarsMoon.bat | 3 + .../cases/system-windows/exitNonZero.bat | 6 + .../cases/system-windows/exitZero.bat | 6 + .../cases/system-windows/passthroughArgs.bat | 1 + .../fixtures/cases/system-windows/project.yml | 45 + .../cases/system-windows/standard.bat | 2 + tests/fixtures/cases/system/cwd.sh | 3 + tests/fixtures/cases/system/envVars.sh | 5 + tests/fixtures/cases/system/envVarsMoon.sh | 7 + tests/fixtures/cases/system/exitNonZero.sh | 8 + tests/fixtures/cases/system/exitZero.sh | 8 + .../fixtures/cases/system/passthroughArgs.sh | 3 + tests/fixtures/cases/system/project.yml | 53 + tests/fixtures/cases/system/standard.sh | 4 + .../fixtures/cases/target-scope-a/project.yml | 17 + .../fixtures/cases/target-scope-b/project.yml | 15 + .../fixtures/cases/target-scope-c/project.yml | 5 + tests/fixtures/cases/tsconfig.json | 7 + tests/fixtures/init-sandbox/package.json | 4 + tests/fixtures/node-npm/.moon/workspace.yml | 9 + tests/fixtures/node-npm/npm/project.yml | 7 + tests/fixtures/node-npm/package.json | 7 + tests/fixtures/node-pnpm/.moon/workspace.yml | 9 + tests/fixtures/node-pnpm/package.json | 7 + tests/fixtures/node-pnpm/pnpm/project.yml | 7 + tests/fixtures/node-yarn1/.moon/workspace.yml | 9 + tests/fixtures/node-yarn1/package.json | 5 + tests/fixtures/node-yarn1/yarn/project.yml | 7 + tests/fixtures/projects/.moon/workspace.yml | 3 +- tests/fixtures/projects/package.json | 5 +- tests/fixtures/tasks/.moon/workspace.yml | 3 +- tests/fixtures/tasks/merge-append/project.yml | 2 +- .../fixtures/tasks/merge-prepend/project.yml | 2 +- .../fixtures/tasks/merge-replace/project.yml | 2 +- tests/fixtures/tasks/package.json | 5 +- 217 files changed, 5540 insertions(+), 2184 deletions(-) create mode 100644 .yarn/versions/5180ba6c.yml delete mode 100644 crates/cli/src/helpers.rs create mode 100644 crates/cli/tests/bin_test.rs create mode 100644 crates/cli/tests/init_test.rs create mode 100644 crates/cli/tests/project_graph_test.rs create mode 100644 crates/cli/tests/project_test.rs create mode 100644 crates/cli/tests/run_test.rs create mode 100644 crates/cli/tests/setup_teardown_test.rs rename crates/cli/{src/commands/snapshots/moon_cli__commands__project_graph__tests__many_projects.snap => tests/snapshots/project_graph_test__many_projects.snap} (94%) rename crates/cli/{src/commands/snapshots/moon_cli__commands__project_graph__tests__no_projects.snap => tests/snapshots/project_graph_test__no_projects.snap} (68%) rename crates/cli/{src/commands/snapshots/moon_cli__commands__project_graph__tests__single_project_no_dependencies.snap => tests/snapshots/project_graph_test__single_project_no_dependencies.snap} (79%) rename crates/cli/{src/commands/snapshots/moon_cli__commands__project_graph__tests__single_project_with_dependencies.snap => tests/snapshots/project_graph_test__single_project_with_dependencies.snap} (89%) rename crates/cli/{src/commands/snapshots/moon_cli__commands__project__tests__advanced_config.snap => tests/snapshots/project_test__advanced_config.snap} (91%) rename crates/cli/{src/commands/snapshots/moon_cli__commands__project__tests__basic_config.snap => tests/snapshots/project_test__basic_config.snap} (89%) rename crates/cli/{src/commands/snapshots/moon_cli__commands__project__tests__depends_on_paths.snap => tests/snapshots/project_test__depends_on_paths.snap} (91%) rename crates/cli/{src/commands/snapshots/moon_cli__commands__project__tests__empty_config.snap => tests/snapshots/project_test__empty_config.snap} (87%) rename crates/cli/{src/commands/snapshots/moon_cli__commands__project__tests__no_config.snap => tests/snapshots/project_test__no_config.snap} (87%) rename crates/cli/{src/commands/snapshots/moon_cli__commands__project__tests__unknown_project.snap => tests/snapshots/project_test__unknown_project.snap} (71%) rename crates/cli/{src/commands/snapshots/moon_cli__commands__project__tests__with_tasks.snap => tests/snapshots/project_test__with_tasks.snap} (90%) create mode 100644 crates/cli/tests/snapshots/run_test__caching__uses_cache_on_subsequent_runs-2.snap create mode 100644 crates/cli/tests/snapshots/run_test__caching__uses_cache_on_subsequent_runs.snap create mode 100644 crates/cli/tests/snapshots/run_test__dependencies__runs_the_graph_in_order.snap create mode 100644 crates/cli/tests/snapshots/run_test__dependencies__runs_the_graph_in_order_not_from_head.snap create mode 100644 crates/cli/tests/snapshots/run_test__errors_for_cycle_in_task_deps.snap create mode 100644 crates/cli/tests/snapshots/run_test__errors_for_unknown_project.snap create mode 100644 crates/cli/tests/snapshots/run_test__errors_for_unknown_task_in_project.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__engines__adds_engines_constraint.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__engines__doesnt_add_engines_constraint.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__handles_process_exit_code_nonzero.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__handles_process_exit_code_zero.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__handles_process_exit_nonzero.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__handles_process_exit_zero.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__handles_unhandled_promise.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__inherits_moon_env_vars.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__passes_args_through.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__runs_cjs_files.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__runs_from_project_root.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__runs_from_workspace_root.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__runs_mjs_files.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__runs_package_managers.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__runs_standard_script.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__sets_env_vars.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__supports_top_level_await.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__sync_depends_on__syncs_as_dependency_to_package_json.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__sync_depends_on__syncs_as_reference_to_tsconfig_json-2.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__sync_depends_on__syncs_as_reference_to_tsconfig_json.snap create mode 100644 crates/cli/tests/snapshots/run_test__node__version_manager__errors_for_invalid_value.snap create mode 100644 crates/cli/tests/snapshots/run_test__node_npm__installs_correct_version.snap create mode 100644 crates/cli/tests/snapshots/run_test__node_pnpm__installs_correct_version.snap create mode 100644 crates/cli/tests/snapshots/run_test__node_yarn1__installs_correct_version.snap create mode 100644 crates/cli/tests/snapshots/run_test__system__handles_echo.snap create mode 100644 crates/cli/tests/snapshots/run_test__system__handles_ls.snap create mode 100644 crates/cli/tests/snapshots/run_test__system__handles_ls_from_root.snap create mode 100644 crates/cli/tests/snapshots/run_test__system__handles_process_exit_nonzero.snap create mode 100644 crates/cli/tests/snapshots/run_test__system__handles_process_exit_zero.snap create mode 100644 crates/cli/tests/snapshots/run_test__system__inherits_moon_env_vars.snap create mode 100644 crates/cli/tests/snapshots/run_test__system__passes_args_through.snap create mode 100644 crates/cli/tests/snapshots/run_test__system__retries_on_failure_till_count.snap create mode 100644 crates/cli/tests/snapshots/run_test__system__runs_bash_script.snap create mode 100644 crates/cli/tests/snapshots/run_test__system__runs_from_project_root.snap create mode 100644 crates/cli/tests/snapshots/run_test__system__runs_from_workspace_root.snap create mode 100644 crates/cli/tests/snapshots/run_test__system__sets_env_vars.snap create mode 100644 crates/cli/tests/snapshots/run_test__system_windows__handles_process_exit_nonzero.snap create mode 100644 crates/cli/tests/snapshots/run_test__system_windows__handles_process_exit_zero.snap create mode 100644 crates/cli/tests/snapshots/run_test__system_windows__inherits_moon_env_vars.snap create mode 100644 crates/cli/tests/snapshots/run_test__system_windows__passes_args_through.snap create mode 100644 crates/cli/tests/snapshots/run_test__system_windows__retries_on_failure_till_count.snap create mode 100644 crates/cli/tests/snapshots/run_test__system_windows__runs_bat_script.snap create mode 100644 crates/cli/tests/snapshots/run_test__system_windows__runs_from_project_root.snap create mode 100644 crates/cli/tests/snapshots/run_test__system_windows__runs_from_workspace_root.snap create mode 100644 crates/cli/tests/snapshots/run_test__system_windows__sets_env_vars.snap create mode 100644 crates/cli/tests/snapshots/run_test__target_scopes__errors_for_deps_scope.snap create mode 100644 crates/cli/tests/snapshots/run_test__target_scopes__errors_for_self_scope.snap create mode 100644 crates/toolchain/src/pms/mod.rs create mode 100644 crates/toolchain/src/pms/npm.rs create mode 100644 crates/toolchain/src/pms/pnpm.rs create mode 100644 crates/toolchain/src/pms/yarn.rs delete mode 100644 crates/toolchain/src/tool.rs delete mode 100644 crates/toolchain/src/tools/npm.rs delete mode 100644 crates/toolchain/src/tools/pnpm.rs delete mode 100644 crates/toolchain/src/tools/yarn.rs create mode 100644 crates/toolchain/src/traits.rs create mode 100644 crates/utils/src/path.rs create mode 100644 tests/fixtures/cases/.moon/workspace.yml create mode 100644 tests/fixtures/cases/base/project.yml create mode 100644 tests/fixtures/cases/depends-on/package.json create mode 100644 tests/fixtures/cases/depends-on/project.yml create mode 100644 tests/fixtures/cases/depends-on/tsconfig.json create mode 100644 tests/fixtures/cases/deps-a/package.json create mode 100644 tests/fixtures/cases/deps-a/project.yml create mode 100644 tests/fixtures/cases/deps-b/package.json create mode 100644 tests/fixtures/cases/deps-b/project.yml create mode 100644 tests/fixtures/cases/deps-b/tsconfig.json create mode 100644 tests/fixtures/cases/deps-c/project.yml create mode 100644 tests/fixtures/cases/deps-c/tsconfig.json create mode 100644 tests/fixtures/cases/node/cjsFile.cjs create mode 100644 tests/fixtures/cases/node/cwd.js create mode 100644 tests/fixtures/cases/node/envVars.js create mode 100644 tests/fixtures/cases/node/envVarsMoon.js create mode 100644 tests/fixtures/cases/node/exitCodeNonZero.js create mode 100644 tests/fixtures/cases/node/exitCodeZero.js create mode 100644 tests/fixtures/cases/node/mjsFile.mjs create mode 100644 tests/fixtures/cases/node/passthroughArgs.js create mode 100644 tests/fixtures/cases/node/processExitNonZero.js create mode 100644 tests/fixtures/cases/node/processExitZero.js create mode 100644 tests/fixtures/cases/node/project.yml create mode 100644 tests/fixtures/cases/node/standard.js create mode 100644 tests/fixtures/cases/node/throwError.js create mode 100644 tests/fixtures/cases/node/topLevelAwait.mjs create mode 100644 tests/fixtures/cases/node/unhandledPromise.js create mode 100644 tests/fixtures/cases/package.json create mode 100644 tests/fixtures/cases/system-windows/cwd.bat create mode 100644 tests/fixtures/cases/system-windows/envVars.bat create mode 100644 tests/fixtures/cases/system-windows/envVarsMoon.bat create mode 100644 tests/fixtures/cases/system-windows/exitNonZero.bat create mode 100644 tests/fixtures/cases/system-windows/exitZero.bat create mode 100644 tests/fixtures/cases/system-windows/passthroughArgs.bat create mode 100644 tests/fixtures/cases/system-windows/project.yml create mode 100644 tests/fixtures/cases/system-windows/standard.bat create mode 100644 tests/fixtures/cases/system/cwd.sh create mode 100644 tests/fixtures/cases/system/envVars.sh create mode 100644 tests/fixtures/cases/system/envVarsMoon.sh create mode 100644 tests/fixtures/cases/system/exitNonZero.sh create mode 100644 tests/fixtures/cases/system/exitZero.sh create mode 100644 tests/fixtures/cases/system/passthroughArgs.sh create mode 100644 tests/fixtures/cases/system/project.yml create mode 100644 tests/fixtures/cases/system/standard.sh create mode 100644 tests/fixtures/cases/target-scope-a/project.yml create mode 100644 tests/fixtures/cases/target-scope-b/project.yml create mode 100644 tests/fixtures/cases/target-scope-c/project.yml create mode 100644 tests/fixtures/cases/tsconfig.json create mode 100644 tests/fixtures/init-sandbox/package.json create mode 100644 tests/fixtures/node-npm/.moon/workspace.yml create mode 100644 tests/fixtures/node-npm/npm/project.yml create mode 100644 tests/fixtures/node-npm/package.json create mode 100644 tests/fixtures/node-pnpm/.moon/workspace.yml create mode 100644 tests/fixtures/node-pnpm/package.json create mode 100644 tests/fixtures/node-pnpm/pnpm/project.yml create mode 100644 tests/fixtures/node-yarn1/.moon/workspace.yml create mode 100644 tests/fixtures/node-yarn1/package.json create mode 100644 tests/fixtures/node-yarn1/yarn/project.yml diff --git a/.eslintignore b/.eslintignore index e8ce1ce5c73..176d671c9fe 100644 --- a/.eslintignore +++ b/.eslintignore @@ -10,4 +10,7 @@ mjs/ umd/ *.min.js *.map -*.snap \ No newline at end of file +*.snap + +# Test fixtures +/tests diff --git a/.github/workflows/moon.yml b/.github/workflows/moon.yml index 8d4754f5d5d..c96d6e5f4fd 100644 --- a/.github/workflows/moon.yml +++ b/.github/workflows/moon.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - node-version: ['12.22.9', '14.19.0', '16.14.0'] + node-version: ['14.19.0', '16.14.0', '18.0.0'] steps: - uses: actions/checkout@v3 with: @@ -50,4 +50,5 @@ jobs: command: run args: -- --logLevel trace ci env: + CLICOLOR_FORCE: true MOON_NODE_VERSION: ${{ matrix.node-version }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c3554b5c6c4..2ab904317a6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -26,8 +26,12 @@ jobs: components: rustfmt - uses: actions-rs/cargo@v1 with: - command: fmt - args: --all -- --check + command: install + args: --debug --force cargo-make + - uses: actions-rs/cargo@v1 + with: + command: make + args: format lint: name: Lint runs-on: ${{ matrix.os }} @@ -53,15 +57,19 @@ jobs: components: clippy - uses: actions-rs/cargo@v1 with: - command: clippy - args: --workspace --all-targets -- --deny warnings + command: install + args: --debug --force cargo-make + - uses: actions-rs/cargo@v1 + with: + command: make + args: lint test: name: Test runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - fail-fast: true + fail-fast: false steps: - uses: actions/checkout@v3 - uses: actions/cache@v3 @@ -77,10 +85,14 @@ jobs: with: toolchain: 1.60.0 profile: minimal + - uses: actions-rs/cargo@v1 + with: + command: install + args: --debug --force cargo-make - uses: actions-rs/cargo@v1 with: command: build - uses: actions-rs/cargo@v1 with: - command: test - args: --workspace + command: make + args: test diff --git a/.gitignore b/.gitignore index f56cad302a3..d96811bc3a1 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,9 @@ packages/*/moon packages/*/moon.exe /moon /moon.exe + +# Moon tests/fixtures +tests/fixtures/*/.moon/cache +tests/fixtures/*/package-lock.json +tests/fixtures/*/pnpm-lock.yaml +tests/fixtures/*/yarn.lock diff --git a/.yarn/versions/5180ba6c.yml b/.yarn/versions/5180ba6c.yml new file mode 100644 index 00000000000..5a5414c28c0 --- /dev/null +++ b/.yarn/versions/5180ba6c.yml @@ -0,0 +1,2 @@ +releases: + "@moonrepo/runtime": patch diff --git a/Cargo.lock b/Cargo.lock index 4b2c926657d..351240ca1a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,12 @@ dependencies = [ "syn", ] +[[package]] +name = "async_once" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce4f10ea3abcd6617873bae9f91d1c5332b4a778bd9ce34d0cd517474c1de82" + [[package]] name = "atomic" version = "0.5.1" @@ -183,6 +189,42 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cached" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aadf76ddea74bab35ebeb8f1eb115b9bc04eaee42d8acc0d5f477dee6b176c9a" +dependencies = [ + "async-trait", + "async_once", + "cached_proc_macro", + "cached_proc_macro_types", + "futures", + "hashbrown 0.12.0", + "lazy_static", + "once_cell", + "thiserror", + "tokio", +] + +[[package]] +name = "cached_proc_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bce0f37f9b77c6b93cdf3f060c89adca303d2ab052cacb3c3d1ab543e8cecd2f" +dependencies = [ + "cached_proc_macro_types", + "darling", + "quote", + "syn", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" + [[package]] name = "cc" version = "1.0.73" @@ -219,9 +261,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.9" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aad2534fad53df1cc12519c5cda696dd3e20e6118a027e24054aea14a0bdcbe" +checksum = "7c167e37342afc5f33fd87bbc870cedd020d2a6dffa05d45ccd9241fbdd146db" dependencies = [ "atty", "bitflags", @@ -353,6 +395,41 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dialoguer" version = "0.9.0" @@ -708,6 +785,12 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "hashbrown" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c21d40587b92fa6a6c6e3c1bdbf87d75511db5672f9c93175574b3a00df1758" + [[package]] name = "heck" version = "0.3.3" @@ -734,9 +817,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" dependencies = [ "bytes", "fnv", @@ -756,9 +839,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" [[package]] name = "httpdate" @@ -803,6 +886,12 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -845,7 +934,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.11.2", ] [[package]] @@ -933,9 +1022,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.123" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd" +checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" [[package]] name = "linked-hash-map" @@ -1057,6 +1146,7 @@ dependencies = [ "indicatif", "insta", "itertools", + "moon_cache", "moon_config", "moon_logger", "moon_project", @@ -1065,6 +1155,7 @@ dependencies = [ "moon_utils", "moon_workspace", "predicates", + "serial_test", "strum", "strum_macros", "tokio", @@ -1104,6 +1195,7 @@ version = "0.1.0" dependencies = [ "chrono", "console", + "dirs", "fern", "log", ] @@ -1165,7 +1257,10 @@ dependencies = [ name = "moon_utils" version = "0.1.0" dependencies = [ + "assert_cmd", + "assert_fs", "async-recursion", + "cached", "chrono", "chrono-humanize", "dirs", @@ -1239,9 +1334,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", @@ -1404,9 +1499,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" @@ -1890,9 +1985,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.91" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52" dependencies = [ "proc-macro2", "quote", @@ -2000,9 +2095,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] @@ -2015,9 +2110,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +checksum = "0f48b6d60512a392e34dbf7fd456249fd2de3c83669ab642e021903f4015185b" dependencies = [ "bytes", "libc", @@ -2088,9 +2183,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" dependencies = [ "proc-macro2", "quote", @@ -2129,9 +2224,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-normalization" diff --git a/Makefile.toml b/Makefile.toml index 971e0bf58ae..d72315a6733 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,33 +1,55 @@ [config] default_to_workspace = false +skip_core_tasks = true [tasks.clean] command = "cargo" args = ["clean"] +# BUILDING + [tasks.build] command = "cargo" args = ["build"] +# FORMATTING + [tasks.format] install_crate = "rustfmt" command = "cargo" args = ["fmt", "--all", "--", "--emit=files"] +# LINTING + [tasks.lint] -install_crate = "rustfmt" command = "cargo" args = ["clippy", "--workspace", "--all-targets"] -dependencies = ["format"] -[tasks.test] +# TESTING + +[tasks.pre-test] +command = "cargo" +args = ["run", "--", "--logLevel", "debug", "setup"] +cwd = "./tests/fixtures/cases" + +[tasks.run-test] command = "cargo" args = ["test", "--workspace"] +[tasks.post-test] +command = "rm" +args = ["-rf", "./tests/fixtures/cases/.moon/cache"] +condition = { platforms = ["mac", "linux"] } + +[tasks.test] +run_task = { name = ["pre-test", "run-test"], fork = true, cleanup_task = "post-test" } + [tasks.test-output] command = "cargo" args = ["test", "--workspace", "--", "--nocapture", "--show-output"] +## OTHER + [tasks.gen-json-schemas] command = "cargo" args = ["run", "-p", "moon_config"] @@ -35,6 +57,7 @@ args = ["run", "-p", "moon_config"] [tasks.check] dependencies = [ "format", + "lint", "build", "test" ] diff --git a/README.md b/README.md index 5df6f5521b1..37818717518 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,21 @@ Moon is a *m*onorepo *o*rganization, *o*rchestration, and *n*otification tool fo projects, written in Rust. Many of the concepts within Moon are heavily inspired from Bazel. - [Documentation](./docs/README.md) + +## Contributing + +Moon is built on Rust and requires `rustup` and `cargo` to exist in your environment. You can [install Rust from the official website](https://www.rust-lang.org/tools/install). + +We also require additional Cargo commands, which can be installed with the following. + +``` +cargo install --force cargo-make +cargo install --force cargo-insta +``` + +To streamline development, we utilize [cargo-make](https://github.com/sagiegurari/cargo-make) for common tasks. + +- `cargo make build` - Builds all crates into a single `moon` binary. +- `cargo make format` - Formats code. +- `cargo make lint` - Runs the linter (clippy). +- `cargo make test` - Runs unit and integration tests. Also sets up the moon toolchain. \ No newline at end of file diff --git a/crates/cache/src/hasher.rs b/crates/cache/src/hasher.rs index 07a551fbb48..6e8bb54b23b 100644 --- a/crates/cache/src/hasher.rs +++ b/crates/cache/src/hasher.rs @@ -1,7 +1,7 @@ use moon_config::package::PackageJson; use moon_config::tsconfig::TsConfigJson; use moon_project::{Project, Task}; -use moon_utils::fs; +use moon_utils::path; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::BTreeMap; @@ -64,9 +64,9 @@ impl Hasher { pub fn hash_inputs(&mut self, inputs: BTreeMap) { for (file, hash) in inputs { // Standardize on `/` separators so that the hash is - // the same between windows and posix machines. + // the same between windows and nix machines. self.input_hashes - .insert(fs::standardize_separators(&file), hash); + .insert(path::standardize_separators(&file), hash); } } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 7ddd89083e5..990e0fb6a34 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -30,6 +30,8 @@ strum_macros = "0.23" tokio = { version = "1.16", features = ["full"] } [dev-dependencies] +moon_cache = { path = "../cache" } assert_cmd = "2.0" insta = "1.8" predicates = "2.0" +serial_test = "0.6" diff --git a/crates/cli/src/commands/bin.rs b/crates/cli/src/commands/bin.rs index d2f44c68889..a3ef9c81fdb 100644 --- a/crates/cli/src/commands/bin.rs +++ b/crates/cli/src/commands/bin.rs @@ -1,6 +1,6 @@ use clap::ArgEnum; use moon_terminal::helpers::safe_exit; -use moon_toolchain::Tool; +use moon_toolchain::{Executable, Installable}; use moon_workspace::Workspace; #[derive(ArgEnum, Clone, Debug)] @@ -16,68 +16,60 @@ enum BinExitCodes { NotInstalled = 2, } -pub async fn bin(tool_type: &BinTools) -> Result<(), Box> { - let workspace = Workspace::load().await?; - let toolchain = &workspace.toolchain; - - let tool: &dyn Tool = match tool_type { - BinTools::Node => toolchain.get_node(), - BinTools::Npm => toolchain.get_npm(), - BinTools::Pnpm => match toolchain.get_pnpm() { - Some(t) => t, - None => { - safe_exit(BinExitCodes::NotConfigured as i32); - } - }, - BinTools::Yarn => match toolchain.get_yarn() { - Some(t) => t, - None => { - safe_exit(BinExitCodes::NotConfigured as i32); - } - }, - }; - - let installed = tool.is_installed(true).await; +async fn is_installed(tool: &dyn Installable, parent: &T) { + let installed = tool.is_installed(parent, true).await; if installed.is_err() || !installed.unwrap() { safe_exit(BinExitCodes::NotInstalled as i32); } +} - println!("{}", tool.get_bin_path().display()); - - Ok(()) +fn not_configured() -> ! { + safe_exit(BinExitCodes::NotConfigured as i32); } -#[cfg(test)] -mod tests { - use crate::helpers::create_test_command; - use predicates::prelude::*; +fn log_bin_path(tool: &dyn Executable) { + println!("{}", tool.get_bin_path().display()); +} - #[test] - fn invalid_tool() { - let assert = create_test_command("base") - .arg("bin") - .arg("unknown") - .assert(); +pub async fn bin(tool_type: &BinTools) -> Result<(), Box> { + let workspace = Workspace::load().await?; + let toolchain = &workspace.toolchain; - assert - .failure() - .code(2) - .stdout("") - .stderr(predicate::str::contains("\"unknown\" isn\'t a valid value")); - } + match tool_type { + BinTools::Node => { + let node = toolchain.get_node(); - #[test] - fn not_configured() { - let assert = create_test_command("base").arg("bin").arg("yarn").assert(); + is_installed(node, toolchain).await; + log_bin_path(node); + } + BinTools::Npm | BinTools::Pnpm | BinTools::Yarn => { + let node = toolchain.get_node(); - assert.failure().code(1).stdout(""); - } + match tool_type { + BinTools::Pnpm => match node.get_pnpm() { + Some(pnpm) => { + is_installed(pnpm, node).await; + log_bin_path(pnpm); + } + None => not_configured(), + }, + BinTools::Yarn => match node.get_yarn() { + Some(yarn) => { + is_installed(yarn, node).await; + log_bin_path(yarn); + } + None => not_configured(), + }, + _ => { + let npm = node.get_npm(); - #[test] - fn not_installed() { - let assert = create_test_command("base").arg("bin").arg("node").assert(); + is_installed(npm, node).await; + log_bin_path(npm); + } + }; + } + }; - assert.failure().code(2).stdout(""); - } + Ok(()) } diff --git a/crates/cli/src/commands/ci.rs b/crates/cli/src/commands/ci.rs index 54b32e0258c..77b84038b1f 100644 --- a/crates/cli/src/commands/ci.rs +++ b/crates/cli/src/commands/ci.rs @@ -5,7 +5,7 @@ use moon_logger::{color, debug}; use moon_project::{Target, TouchedFilePaths}; use moon_terminal::helpers::{replace_style_tokens, safe_exit}; use moon_terminal::output; -use moon_utils::{fs, is_ci, time}; +use moon_utils::{is_ci, path, time}; use moon_workspace::DepGraph; use moon_workspace::{ActionRunner, ActionStatus, Workspace, WorkspaceError}; use std::collections::HashSet; @@ -68,7 +68,7 @@ async fn gather_touched_files( .iter() .map(|f| { touched_files_to_print.push(format!(" {}", color::file(f))); - workspace.root.join(fs::normalize_separators(f)) + workspace.root.join(path::normalize_separators(f)) }) .collect(); @@ -222,7 +222,7 @@ pub async fn ci(options: CiOptions) -> Result<(), Box> { ActionStatus::Passed | ActionStatus::Cached | ActionStatus::Skipped => { color::success("pass") } - ActionStatus::Failed => color::failure("fail"), + ActionStatus::Failed | ActionStatus::FailedAndAbort => color::failure("fail"), ActionStatus::Invalid => color::invalid("warn"), _ => color::muted_light("oops"), }; @@ -237,10 +237,6 @@ pub async fn ci(options: CiOptions) -> Result<(), Box> { meta.push(time::elapsed(result.duration.unwrap())); } - if result.exit_code > 0 { - meta.push(format!("exit code {}", result.exit_code)); - } - term.write_line(&format!( "{} {} {}", status, diff --git a/crates/cli/src/commands/init.rs b/crates/cli/src/commands/init.rs index 1aa7ee27bea..14b7fc39a10 100644 --- a/crates/cli/src/commands/init.rs +++ b/crates/cli/src/commands/init.rs @@ -5,14 +5,19 @@ use moon_utils::fs; use std::env; use std::fs::OpenOptions; use std::io::prelude::*; +use std::path::PathBuf; pub async fn init(dest: &str, force: bool) -> Result<(), Box> { let working_dir = env::current_dir().unwrap(); + let dest_path = PathBuf::from(dest); let dest_dir = if dest == "." { working_dir + } else if dest_path.is_absolute() { + dest_path } else { working_dir.join(dest) }; + let moon_dir = dest_dir.join(CONFIG_DIRNAME); if moon_dir.exists() && !force { @@ -50,8 +55,7 @@ pub async fn init(dest: &str, force: bool) -> Result<(), Box Result<(), Box) -> Result<(), Box { pass_count += 1; } - ActionStatus::Failed => { + ActionStatus::Failed | ActionStatus::FailedAndAbort => { fail_count += 1; } ActionStatus::Invalid => { @@ -133,7 +134,10 @@ pub fn render_result_stats( term.write_line("")?; let counts_message = counts_message.join(&color::muted(", ")); - let elapsed_time = time::elapsed(duration); + let elapsed_time = match env::var("MOON_TEST") { + Ok(_) => String::from("100ms"), // Snapshots + Err(_) => time::elapsed(duration), + }; if in_actions_context { term.render_entry("Actions", &counts_message)?; diff --git a/crates/cli/src/commands/setup.rs b/crates/cli/src/commands/setup.rs index d8e3ff709f5..cfe99e59bfe 100644 --- a/crates/cli/src/commands/setup.rs +++ b/crates/cli/src/commands/setup.rs @@ -1,26 +1,9 @@ use moon_workspace::Workspace; pub async fn setup() -> Result<(), Box> { - let workspace = Workspace::load().await?; - let mut root_package = workspace.load_package_json().await?; + let mut workspace = Workspace::load().await?; - workspace.toolchain.setup(&mut root_package, true).await?; + workspace.toolchain.setup(true).await?; Ok(()) } - -// #[cfg(test)] -// mod tests { -// use crate::helpers::create_test_command; - -// #[test] -// fn installs() { -// let assert = create_test_command("base") -// .arg("--log-level") -// .arg("trace") -// .arg("setup") -// .assert(); - -// assert.success().code(0); -// } -// } diff --git a/crates/cli/src/commands/teardown.rs b/crates/cli/src/commands/teardown.rs index 22c49e87a48..c286a9a5f2c 100644 --- a/crates/cli/src/commands/teardown.rs +++ b/crates/cli/src/commands/teardown.rs @@ -1,21 +1,9 @@ use moon_workspace::Workspace; pub async fn teardown() -> Result<(), Box> { - let workspace = Workspace::load().await?; + let mut workspace = Workspace::load().await?; workspace.toolchain.teardown().await?; Ok(()) } - -// #[cfg(test)] -// mod tests { -// use crate::helpers::create_test_command; - -// #[test] -// fn uninstalls() { -// let assert = create_test_command("base").arg("teardown").assert(); - -// assert.success().code(0); -// } -// } diff --git a/crates/cli/src/helpers.rs b/crates/cli/src/helpers.rs deleted file mode 100644 index e0b1b0fbd3c..00000000000 --- a/crates/cli/src/helpers.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[cfg(test)] -pub fn create_test_command(fixture: &str) -> assert_cmd::Command { - let mut path = std::env::current_dir().unwrap(); - path.push("../../tests/fixtures"); - path.push(fixture); - - let mut cmd = assert_cmd::Command::cargo_bin("moon").unwrap(); - cmd.current_dir(path.canonicalize().unwrap()); - cmd.env("MOON_TEST", "true"); - cmd -} - -#[cfg(test)] -pub fn get_assert_output(assert: &assert_cmd::assert::Assert) -> String { - String::from_utf8(assert.get_output().stdout.to_owned()).unwrap() -} - -#[cfg(test)] -pub fn get_assert_stderr_output(assert: &assert_cmd::assert::Assert) -> String { - String::from_utf8(assert.get_output().stderr.to_owned()).unwrap() -} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 01590eee8f2..00e823adc20 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,7 +1,6 @@ mod app; mod commands; mod enums; -mod helpers; use crate::commands::bin::bin; use crate::commands::ci::{ci, CiOptions}; diff --git a/crates/cli/tests/bin_test.rs b/crates/cli/tests/bin_test.rs new file mode 100644 index 00000000000..b052ad7ba0b --- /dev/null +++ b/crates/cli/tests/bin_test.rs @@ -0,0 +1,52 @@ +use moon_utils::test::create_moon_command; +use predicates::prelude::*; + +// This requires installing the toolchain which is quite heavy in tests! +// #[test] +// fn valid_tool() { +// let assert = create_moon_command("cases").arg("bin").arg("node").assert(); + +// assert +// .success() +// .code(0) +// .stdout("") +// .stderr(predicate::str::contains("\"unknown\" isn\'t a valid value")); +// } + +#[test] +fn invalid_tool() { + let assert = create_moon_command("cases") + .arg("bin") + .arg("unknown") + .assert(); + + assert + .failure() + .code(2) + .stdout("") + .stderr(predicate::str::contains("\"unknown\" isn\'t a valid value")); +} + +// We use a different Node.js version as to not conflict with other tests! + +#[test] +fn not_configured() { + let assert = create_moon_command("cases") + .arg("bin") + .arg("yarn") + .env("MOON_NODE_VERSION", "17.0.0") + .assert(); + + assert.failure().code(1).stdout(""); +} + +#[test] +fn not_installed() { + let assert = create_moon_command("cases") + .arg("bin") + .arg("node") + .env("MOON_NODE_VERSION", "17.0.0") + .assert(); + + assert.failure().code(2).stdout(""); +} diff --git a/crates/cli/tests/init_test.rs b/crates/cli/tests/init_test.rs new file mode 100644 index 00000000000..edeab6ff009 --- /dev/null +++ b/crates/cli/tests/init_test.rs @@ -0,0 +1,164 @@ +use moon_config::{load_global_project_config_template, load_workspace_config_template}; +use moon_utils::test::{create_moon_command, get_fixtures_dir}; +use predicates::prelude::*; +use serial_test::serial; +use std::fs; +use std::path::PathBuf; + +fn cleanup_sandbox(root: PathBuf) { + fs::remove_dir_all(root.join(".moon")).unwrap(); + fs::remove_file(root.join(".gitignore")).unwrap(); +} + +#[test] +#[serial] +fn creates_files_in_dest() { + let root = get_fixtures_dir("init-sandbox"); + let workspace_config = root.join(".moon").join("workspace.yml"); + let project_config = root.join(".moon").join("project.yml"); + let gitignore = root.join(".gitignore"); + + assert!(!workspace_config.exists()); + assert!(!project_config.exists()); + assert!(!gitignore.exists()); + + let assert = create_moon_command("init-sandbox") + .arg("init") + .arg(&root) + .assert(); + + assert.success().code(0).stdout(predicate::str::starts_with( + "Moon has successfully been initialized in", + )); + + assert!(workspace_config.exists()); + assert!(project_config.exists()); + assert!(gitignore.exists()); + + cleanup_sandbox(root); +} + +#[test] +#[serial] +fn creates_workspace_config_from_template() { + let root = get_fixtures_dir("init-sandbox"); + let workspace_config = root.join(".moon").join("workspace.yml"); + + create_moon_command("init-sandbox") + .arg("init") + .arg(&root) + .assert(); + + assert_eq!( + fs::read_to_string(workspace_config).unwrap(), + load_workspace_config_template() + ); + + cleanup_sandbox(root); +} + +#[test] +#[serial] +fn creates_project_config_from_template() { + let root = get_fixtures_dir("init-sandbox"); + let project_config = root.join(".moon").join("project.yml"); + + create_moon_command("init-sandbox") + .arg("init") + .arg(&root) + .assert(); + + assert_eq!( + fs::read_to_string(project_config).unwrap(), + load_global_project_config_template() + ); + + cleanup_sandbox(root); +} + +#[test] +#[serial] +fn creates_gitignore_file() { + let root = get_fixtures_dir("init-sandbox"); + let gitignore = root.join(".gitignore"); + + create_moon_command("init-sandbox") + .arg("init") + .arg(&root) + .assert(); + + assert_eq!( + fs::read_to_string(gitignore).unwrap(), + "\n# Moon\n.moon/cache\n" + ); + + cleanup_sandbox(root); +} + +#[test] +#[serial] +fn appends_existing_gitignore_file() { + let root = get_fixtures_dir("init-sandbox"); + let gitignore = root.join(".gitignore"); + + fs::write(&gitignore, "*.js\n*.log").unwrap(); + + create_moon_command("init-sandbox") + .arg("init") + .arg(&root) + .assert(); + + assert_eq!( + fs::read_to_string(gitignore).unwrap(), + "*.js\n*.log\n# Moon\n.moon/cache\n" + ); + + cleanup_sandbox(root); +} + +#[test] +#[serial] +fn doesnt_overwrite_existing_config() { + let root = get_fixtures_dir("init-sandbox"); + + create_moon_command("init-sandbox") + .arg("init") + .arg(&root) + .assert(); + + // Run again + let assert = create_moon_command("init-sandbox") + .arg("init") + .arg(&root) + .assert(); + + assert.success().code(0).stdout(predicate::str::starts_with( + "Moon has already been initialized in", + )); + + cleanup_sandbox(root); +} + +#[test] +#[serial] +fn does_overwrite_existing_config_if_force_passed() { + let root = get_fixtures_dir("init-sandbox"); + + create_moon_command("init-sandbox") + .arg("init") + .arg(&root) + .assert(); + + // Run again + let assert = create_moon_command("init-sandbox") + .arg("init") + .arg(&root) + .arg("--force") + .assert(); + + assert.success().code(0).stdout(predicate::str::starts_with( + "Moon has successfully been initialized in", + )); + + cleanup_sandbox(root); +} diff --git a/crates/cli/tests/project_graph_test.rs b/crates/cli/tests/project_graph_test.rs new file mode 100644 index 00000000000..4de60f3155c --- /dev/null +++ b/crates/cli/tests/project_graph_test.rs @@ -0,0 +1,38 @@ +use insta::assert_snapshot; +use moon_utils::test::{create_moon_command, get_assert_output}; + +#[test] +fn no_projects() { + let assert = create_moon_command("base").arg("project-graph").assert(); + + assert_snapshot!(get_assert_output(&assert)); +} + +#[test] +fn many_projects() { + let assert = create_moon_command("projects") + .arg("project-graph") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); +} + +#[test] +fn single_project_with_dependencies() { + let assert = create_moon_command("projects") + .arg("project-graph") + .arg("foo") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); +} + +#[test] +fn single_project_no_dependencies() { + let assert = create_moon_command("projects") + .arg("project-graph") + .arg("baz") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); +} diff --git a/crates/cli/tests/project_test.rs b/crates/cli/tests/project_test.rs new file mode 100644 index 00000000000..052a5c02690 --- /dev/null +++ b/crates/cli/tests/project_test.rs @@ -0,0 +1,95 @@ +use insta::assert_snapshot; +use moon_utils::test::{create_moon_command, get_assert_output, get_assert_stderr_output}; + +fn force_ansi_colors() { + std::env::set_var("CLICOLOR_FORCE", "1"); +} + +#[test] +fn unknown_project() { + force_ansi_colors(); + + let assert = create_moon_command("projects") + .arg("project") + .arg("unknown") + .assert(); + + assert_snapshot!(get_assert_stderr_output(&assert)); + + assert.failure().code(1); +} + +#[test] +fn empty_config() { + force_ansi_colors(); + + let assert = create_moon_command("projects") + .arg("project") + .arg("emptyConfig") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); +} + +#[test] +fn no_config() { + force_ansi_colors(); + + let assert = create_moon_command("projects") + .arg("project") + .arg("noConfig") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); +} + +#[test] +fn basic_config() { + force_ansi_colors(); + + // with dependsOn and fileGroups + let assert = create_moon_command("projects") + .arg("project") + .arg("basic") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); +} + +#[test] +fn advanced_config() { + force_ansi_colors(); + + // with project metadata + let assert = create_moon_command("projects") + .arg("project") + .arg("advanced") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); +} + +#[test] +fn depends_on_paths() { + force_ansi_colors(); + + // shows dependsOn paths when they exist + let assert = create_moon_command("projects") + .arg("project") + .arg("foo") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); +} + +#[test] +fn with_tasks() { + force_ansi_colors(); + + let assert = create_moon_command("projects") + .arg("project") + .arg("tasks") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); +} diff --git a/crates/cli/tests/run_test.rs b/crates/cli/tests/run_test.rs new file mode 100644 index 00000000000..3ac7e576a5d --- /dev/null +++ b/crates/cli/tests/run_test.rs @@ -0,0 +1,1048 @@ +use insta::assert_snapshot; +use moon_utils::path::replace_home_dir; +use moon_utils::test::{ + create_fixtures_sandbox, create_moon_command, create_moon_command_in, get_assert_output, + replace_fixtures_dir, +}; +use predicates::prelude::*; +use serial_test::serial; +use std::fs::{read_to_string, OpenOptions}; +use std::io::prelude::*; +use std::path::Path; + +fn append_workspace_config(path: &Path, yaml: &str) { + let mut file = OpenOptions::new() + .write(true) + .append(true) + .open(path) + .unwrap(); + + writeln!(file, "{}", yaml).unwrap(); +} + +fn get_path_safe_output(assert: &assert_cmd::assert::Assert, fixtures_dir: &Path) -> String { + let result = replace_home_dir(&replace_fixtures_dir( + &get_assert_output(assert), + fixtures_dir, + )); + + result.replace("/private<", "<") +} + +#[test] +fn errors_for_unknown_project() { + let assert = create_moon_command("cases") + .arg("run") + .arg("unknown:test") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); +} + +#[test] +fn errors_for_unknown_task_in_project() { + let assert = create_moon_command("cases") + .arg("run") + .arg("base:unknown") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); +} + +#[test] +fn errors_for_cycle_in_task_deps() { + let assert = create_moon_command("cases") + .arg("run") + .arg("depsA:taskCycle") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); +} + +mod caching { + use super::*; + use moon_cache::{CacheItem, RunTargetState}; + + #[test] + fn uses_cache_on_subsequent_runs() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn creates_runfile() { + let fixture = create_fixtures_sandbox("cases"); + + create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .assert(); + + assert!(fixture + .path() + .join(".moon/cache/runs/node/runfile.json") + .exists()); + } + + #[tokio::test] + async fn creates_run_state_cache() { + let fixture = create_fixtures_sandbox("cases"); + + create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .assert(); + + let cache_path = fixture + .path() + .join(".moon/cache/runs/node/standard/lastRunState.json"); + + assert!(cache_path.exists()); + + let state = CacheItem::load(cache_path, RunTargetState::default()) + .await + .unwrap(); + + assert_eq!(state.item.exit_code, 0); + assert_eq!(state.item.stdout, "stdout"); + assert_eq!(state.item.stderr, "stderr"); + assert_eq!(state.item.target, "node:standard"); + assert_eq!( + state.item.hash, + "0957f07cd32663feeac762e10189d4be02c100309ed3e28c9d7491f1e040960d" + ); + } +} + +mod dependencies { + use super::*; + + #[test] + fn runs_the_graph_in_order() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("depsA:dependencyOrder") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn runs_the_graph_in_order_not_from_head() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("depsB:dependencyOrder") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } +} + +mod target_scopes { + use super::*; + + #[test] + fn errors_for_deps_scope() { + let assert = create_moon_command("cases") + .arg("run") + .arg("^:test") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn errors_for_self_scope() { + let assert = create_moon_command("cases") + .arg("run") + .arg("~:test") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn supports_all_scope() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg(":all") + .assert(); + let output = get_assert_output(&assert); + + assert!(predicate::str::contains("targetScopeA:all").eval(&output)); + assert!(predicate::str::contains("targetScopeB:all").eval(&output)); + assert!(predicate::str::contains("targetScopeC:all").eval(&output)); + assert!(predicate::str::contains("Tasks: 3 completed").eval(&output)); + } + + #[test] + fn supports_deps_scope_in_task() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("targetScopeA:deps") + .assert(); + let output = get_assert_output(&assert); + + assert!(predicate::str::contains("targetScopeA:deps").eval(&output)); + assert!(predicate::str::contains("scope=deps").eval(&output)); + assert!(predicate::str::contains("depsA:standard").eval(&output)); + assert!(predicate::str::contains("deps=a").eval(&output)); + assert!(predicate::str::contains("depsB:standard").eval(&output)); + assert!(predicate::str::contains("deps=b").eval(&output)); + assert!(predicate::str::contains("depsC:standard").eval(&output)); + assert!(predicate::str::contains("deps=c").eval(&output)); + assert!(predicate::str::contains("Tasks: 4 completed").eval(&output)); + } + + #[test] + fn supports_self_scope_in_task() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("targetScopeB:self") + .assert(); + let output = get_assert_output(&assert); + + assert!(predicate::str::contains("targetScopeB:self").eval(&output)); + assert!(predicate::str::contains("scope=self").eval(&output)); + assert!(predicate::str::contains("targetScopeB:selfOther").eval(&output)); + assert!(predicate::str::contains("selfOther").eval(&output)); + assert!(predicate::str::contains("Tasks: 2 completed").eval(&output)); + } +} + +mod node { + use super::*; + + #[test] + fn runs_package_managers() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:npm") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn runs_standard_script() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn runs_cjs_files() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:cjs") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn runs_mjs_files() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:mjs") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn supports_top_level_await() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:topLevelAwait") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn handles_process_exit_zero() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:processExitZero") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn handles_process_exit_nonzero() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:processExitNonZero") + .assert(); + + if cfg!(windows) { + assert.code(1); + } else { + assert_snapshot!(get_assert_output(&assert)); + } + } + + #[test] + fn handles_process_exit_code_zero() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:exitCodeZero") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn handles_process_exit_code_nonzero() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:exitCodeNonZero") + .assert(); + + if cfg!(windows) { + assert.code(1); + } else { + assert_snapshot!(get_assert_output(&assert)); + } + } + + #[test] + fn handles_throw_error() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:throwError") + .assert(); + let output = get_assert_output(&assert); + + // Output contains file paths that we cant snapshot + assert!(predicate::str::contains("Error: Oops").eval(&output)); + } + + #[test] + fn handles_unhandled_promise() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:unhandledPromise") + .assert(); + + if cfg!(windows) { + assert.code(1); + } else { + assert_snapshot!(get_path_safe_output(&assert, fixture.path())); + } + } + + #[test] + fn passes_args_through() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:passthroughArgs") + .arg("--") + .arg("-aBc") + .arg("--opt") + .arg("value") + .arg("--optCamel=value") + .arg("foo") + .arg("'bar baz'") + .arg("--opt-kebab") + .arg("123") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn sets_env_vars() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:envVars") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn inherits_moon_env_vars() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:envVarsMoon") + .assert(); + + assert_snapshot!(get_path_safe_output(&assert, fixture.path())); + } + + #[test] + fn runs_from_project_root() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:runFromProject") + .assert(); + + assert_snapshot!(get_path_safe_output(&assert, fixture.path())); + } + + #[test] + fn runs_from_workspace_root() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:runFromWorkspace") + .assert(); + + assert_snapshot!(get_path_safe_output(&assert, fixture.path())); + } + + #[test] + fn retries_on_failure_till_count() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:retryCount") + .assert(); + let output = get_assert_output(&assert); + + assert!(predicate::str::contains("Process ~/.moon/tools/node/16.0.0").eval(&output)); + } + + mod install_deps { + use super::*; + + #[test] + fn installs_on_first_run() { + let fixture = create_fixtures_sandbox("cases"); + + assert!(!fixture.path().join("node_modules").exists()); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .env_remove("MOON_TEST_HIDE_INSTALL_OUTPUT") + .assert(); + let output = get_assert_output(&assert); + + assert!(fixture.path().join("node_modules").exists()); + + assert!(predicate::str::contains("added 7 packages").eval(&output)); + } + + #[test] + fn doesnt_reinstall_on_second_run() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .env_remove("MOON_TEST_HIDE_INSTALL_OUTPUT") + .assert(); + let output1 = get_assert_output(&assert); + + assert!(predicate::str::contains("added 7 packages").eval(&output1)); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .env_remove("MOON_TEST_HIDE_INSTALL_OUTPUT") + .assert(); + let output2 = get_assert_output(&assert); + + assert!(!predicate::str::contains("added 7 packages").eval(&output2)); + } + + #[test] + fn creates_workspace_state_cache() { + let fixture = create_fixtures_sandbox("cases"); + + create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .assert(); + + assert!(fixture + .path() + .join(".moon/cache/workspaceState.json") + .exists()); + } + } + + mod engines { + use super::*; + + #[test] + fn adds_engines_constraint() { + let fixture = create_fixtures_sandbox("cases"); + + append_workspace_config( + &fixture.path().join(".moon/workspace.yml"), + r#" addEnginesConstraint: true"#, + ); + + create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .assert(); + + assert_snapshot!(read_to_string(fixture.path().join("package.json")).unwrap()); + } + + #[test] + fn doesnt_add_engines_constraint() { + let fixture = create_fixtures_sandbox("cases"); + + append_workspace_config( + &fixture.path().join(".moon/workspace.yml"), + r#" addEnginesConstraint: false"#, + ); + + create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .assert(); + + assert_snapshot!(read_to_string(fixture.path().join("package.json")).unwrap()); + } + } + + mod version_manager { + use super::*; + + #[test] + fn adds_no_file_by_default() { + let fixture = create_fixtures_sandbox("cases"); + + create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .assert(); + + assert!(!fixture.path().join(".nvmrc").exists()); + assert!(!fixture.path().join(".node-version").exists()); + } + + #[test] + fn adds_nvmrc_file() { + let fixture = create_fixtures_sandbox("cases"); + + append_workspace_config( + &fixture.path().join(".moon/workspace.yml"), + r#" syncVersionManagerConfig: nvm"#, + ); + + create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .assert(); + + assert!(fixture.path().join(".nvmrc").exists()); + + assert_eq!( + read_to_string(fixture.path().join(".nvmrc")).unwrap(), + "16.0.0" + ); + } + + #[test] + fn adds_nodenv_file() { + let fixture = create_fixtures_sandbox("cases"); + + append_workspace_config( + &fixture.path().join(".moon/workspace.yml"), + r#" syncVersionManagerConfig: nodenv"#, + ); + + create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .assert(); + + assert!(fixture.path().join(".node-version").exists()); + + assert_eq!( + read_to_string(fixture.path().join(".node-version")).unwrap(), + "16.0.0" + ); + } + + #[test] + fn errors_for_invalid_value() { + let fixture = create_fixtures_sandbox("cases"); + + append_workspace_config( + &fixture.path().join(".moon/workspace.yml"), + r#" syncVersionManagerConfig: invalid"#, + ); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("node:standard") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + } + + mod sync_depends_on { + use super::*; + + #[test] + fn syncs_as_dependency_to_package_json() { + let fixture = create_fixtures_sandbox("cases"); + + append_workspace_config( + &fixture.path().join(".moon/workspace.yml"), + " syncProjectWorkspaceDependencies: true", + ); + + create_moon_command_in(fixture.path()) + .arg("run") + .arg("dependsOn:standard") + .assert(); + + // deps-c does not have a `package.json` on purpose + assert_snapshot!( + read_to_string(fixture.path().join("depends-on/package.json")).unwrap() + ); + } + + #[test] + fn syncs_as_reference_to_tsconfig_json() { + let fixture = create_fixtures_sandbox("cases"); + + append_workspace_config( + &fixture.path().join(".moon/workspace.yml"), + "typescript:\n syncProjectReferences: true", + ); + + create_moon_command_in(fixture.path()) + .arg("run") + .arg("dependsOn:standard") + .assert(); + + // root + assert_snapshot!(read_to_string(fixture.path().join("tsconfig.json")).unwrap()); + + // project + // deps-a does not have a `tsconfig.json` on purpose + assert_snapshot!( + read_to_string(fixture.path().join("depends-on/tsconfig.json")).unwrap() + ); + } + } +} + +mod node_npm { + use super::*; + + #[test] + #[serial] + fn installs_correct_version() { + let fixture = create_fixtures_sandbox("node-npm"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("npm:version") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + #[serial] + fn can_install_a_dep() { + let fixture = create_fixtures_sandbox("node-npm"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("npm:installDep") + .assert(); + + assert.success(); + } +} + +mod node_pnpm { + use super::*; + + #[test] + #[serial] + fn installs_correct_version() { + let fixture = create_fixtures_sandbox("node-pnpm"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("pnpm:version") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + #[serial] + fn can_install_a_dep() { + let fixture = create_fixtures_sandbox("node-pnpm"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("pnpm:installDep") + .assert(); + + assert.success(); + } +} + +mod node_yarn1 { + use super::*; + + #[test] + #[serial] + fn installs_correct_version() { + let fixture = create_fixtures_sandbox("node-yarn1"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("yarn:version") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + #[serial] + fn can_install_a_dep() { + let fixture = create_fixtures_sandbox("node-yarn1"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("yarn:installDep") + .assert(); + + assert.success(); + } +} + +#[cfg(not(windows))] +mod system { + use super::*; + + #[test] + fn handles_echo() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("system:echo") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn handles_ls() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("system:ls") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn runs_bash_script() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("system:bash") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn handles_process_exit_zero() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("system:exitZero") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn handles_process_exit_nonzero() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("system:exitNonZero") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn passes_args_through() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("system:passthroughArgs") + .arg("--") + .arg("-aBc") + .arg("--opt") + .arg("value") + .arg("--optCamel=value") + .arg("foo") + .arg("'bar baz'") + .arg("--opt-kebab") + .arg("123") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn sets_env_vars() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("system:envVars") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn inherits_moon_env_vars() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("system:envVarsMoon") + .assert(); + + assert_snapshot!(get_path_safe_output(&assert, fixture.path())); + } + + #[test] + fn runs_from_project_root() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("system:runFromProject") + .assert(); + + assert_snapshot!(get_path_safe_output(&assert, fixture.path())); + } + + #[test] + fn runs_from_workspace_root() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("system:runFromWorkspace") + .assert(); + + assert_snapshot!(get_path_safe_output(&assert, fixture.path())); + } + + #[test] + fn retries_on_failure_till_count() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("system:retryCount") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } +} + +#[cfg(windows)] +mod system_windows { + use super::*; + + #[test] + fn runs_bat_script() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("systemWindows:bat") + .assert(); + + assert_snapshot!(get_path_safe_output(&assert, fixture.path())); + } + + #[test] + fn handles_process_exit_zero() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("systemWindows:exitZero") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn handles_process_exit_nonzero() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("systemWindows:exitNonZero") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn passes_args_through() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("systemWindows:passthroughArgs") + .arg("--") + .arg("-aBc") + .arg("--opt") + .arg("value") + .arg("--optCamel=value") + .arg("foo") + .arg("'bar baz'") + .arg("--opt-kebab") + .arg("123") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn sets_env_vars() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("systemWindows:envVars") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } + + #[test] + fn inherits_moon_env_vars() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("systemWindows:envVarsMoon") + .assert(); + + assert_snapshot!(get_path_safe_output(&assert, fixture.path())); + } + + #[test] + fn runs_from_project_root() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("systemWindows:runFromProject") + .assert(); + + assert_snapshot!(get_path_safe_output(&assert, fixture.path())); + } + + #[test] + fn runs_from_workspace_root() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("systemWindows:runFromWorkspace") + .assert(); + + assert_snapshot!(get_path_safe_output(&assert, fixture.path())); + } + + #[test] + fn retries_on_failure_till_count() { + let fixture = create_fixtures_sandbox("cases"); + + let assert = create_moon_command_in(fixture.path()) + .arg("run") + .arg("systemWindows:retryCount") + .assert(); + + assert_snapshot!(get_assert_output(&assert)); + } +} diff --git a/crates/cli/tests/setup_teardown_test.rs b/crates/cli/tests/setup_teardown_test.rs new file mode 100644 index 00000000000..47aa5792db7 --- /dev/null +++ b/crates/cli/tests/setup_teardown_test.rs @@ -0,0 +1,39 @@ +use moon_utils::is_ci; +use moon_utils::path::get_home_dir; +use moon_utils::test::{create_fixtures_sandbox, create_moon_command_in}; + +#[test] +fn sets_up_and_tears_down() { + // This is heavy so avoid in local tests for now + if !is_ci() { + return; + } + + // We use a different Node.js version as to not conflict with other tests! + let node_version = "17.1.0"; + let home_dir = get_home_dir().unwrap(); + let moon_dir = home_dir.join(".moon"); + let node_dir = moon_dir.join("tools/node").join(node_version); + + assert!(!node_dir.exists()); + + let fixture = create_fixtures_sandbox("cases"); + + let setup = create_moon_command_in(fixture.path()) + .arg("setup") + .env("MOON_NODE_VERSION", node_version) + .assert(); + + setup.success().code(0); + + assert!(node_dir.exists()); + + let teardown = create_moon_command_in(fixture.path()) + .arg("teardown") + .env("MOON_NODE_VERSION", node_version) + .assert(); + + teardown.success().code(0); + + assert!(!node_dir.exists()); +} diff --git a/crates/cli/src/commands/snapshots/moon_cli__commands__project_graph__tests__many_projects.snap b/crates/cli/tests/snapshots/project_graph_test__many_projects.snap similarity index 94% rename from crates/cli/src/commands/snapshots/moon_cli__commands__project_graph__tests__many_projects.snap rename to crates/cli/tests/snapshots/project_graph_test__many_projects.snap index e839e31a06d..1e53b1cb65c 100644 --- a/crates/cli/src/commands/snapshots/moon_cli__commands__project_graph__tests__many_projects.snap +++ b/crates/cli/tests/snapshots/project_graph_test__many_projects.snap @@ -1,8 +1,7 @@ --- -source: crates/cli/src/commands/project_graph.rs -assertion_line: 38 +source: crates/cli/tests/project_graph_test.rs +assertion_line: 17 expression: get_assert_output(&assert) - --- digraph { 0 [ label="(workspace)" style=filled, shape=circle, fillcolor=black, fontcolor=white] diff --git a/crates/cli/src/commands/snapshots/moon_cli__commands__project_graph__tests__no_projects.snap b/crates/cli/tests/snapshots/project_graph_test__no_projects.snap similarity index 68% rename from crates/cli/src/commands/snapshots/moon_cli__commands__project_graph__tests__no_projects.snap rename to crates/cli/tests/snapshots/project_graph_test__no_projects.snap index 21f286622ad..705fa0ad283 100644 --- a/crates/cli/src/commands/snapshots/moon_cli__commands__project_graph__tests__no_projects.snap +++ b/crates/cli/tests/snapshots/project_graph_test__no_projects.snap @@ -1,8 +1,7 @@ --- -source: crates/cli/src/commands/project_graph.rs -assertion_line: 29 +source: crates/cli/tests/project_graph_test.rs +assertion_line: 8 expression: get_assert_output(&assert) - --- digraph { 0 [ label="(workspace)" style=filled, shape=circle, fillcolor=black, fontcolor=white] diff --git a/crates/cli/src/commands/snapshots/moon_cli__commands__project_graph__tests__single_project_no_dependencies.snap b/crates/cli/tests/snapshots/project_graph_test__single_project_no_dependencies.snap similarity index 79% rename from crates/cli/src/commands/snapshots/moon_cli__commands__project_graph__tests__single_project_no_dependencies.snap rename to crates/cli/tests/snapshots/project_graph_test__single_project_no_dependencies.snap index d269e773007..7752d3d4f2b 100644 --- a/crates/cli/src/commands/snapshots/moon_cli__commands__project_graph__tests__single_project_no_dependencies.snap +++ b/crates/cli/tests/snapshots/project_graph_test__single_project_no_dependencies.snap @@ -1,8 +1,7 @@ --- -source: crates/cli/src/commands/project_graph.rs -assertion_line: 58 +source: crates/cli/tests/project_graph_test.rs +assertion_line: 37 expression: get_assert_output(&assert) - --- digraph { 0 [ label="(workspace)" style=filled, shape=circle, fillcolor=black, fontcolor=white] diff --git a/crates/cli/src/commands/snapshots/moon_cli__commands__project_graph__tests__single_project_with_dependencies.snap b/crates/cli/tests/snapshots/project_graph_test__single_project_with_dependencies.snap similarity index 89% rename from crates/cli/src/commands/snapshots/moon_cli__commands__project_graph__tests__single_project_with_dependencies.snap rename to crates/cli/tests/snapshots/project_graph_test__single_project_with_dependencies.snap index e389b848373..ff16b8df87e 100644 --- a/crates/cli/src/commands/snapshots/moon_cli__commands__project_graph__tests__single_project_with_dependencies.snap +++ b/crates/cli/tests/snapshots/project_graph_test__single_project_with_dependencies.snap @@ -1,8 +1,7 @@ --- -source: crates/cli/src/commands/project_graph.rs -assertion_line: 48 +source: crates/cli/tests/project_graph_test.rs +assertion_line: 27 expression: get_assert_output(&assert) - --- digraph { 0 [ label="(workspace)" style=filled, shape=circle, fillcolor=black, fontcolor=white] diff --git a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__advanced_config.snap b/crates/cli/tests/snapshots/project_test__advanced_config.snap similarity index 91% rename from crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__advanced_config.snap rename to crates/cli/tests/snapshots/project_test__advanced_config.snap index 8009127bd3c..db9c5de8697 100644 --- a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__advanced_config.snap +++ b/crates/cli/tests/snapshots/project_test__advanced_config.snap @@ -1,6 +1,6 @@ --- -source: crates/cli/src/commands/project.rs -assertion_line: 171 +source: crates/cli/tests/project_test.rs +assertion_line: 69 expression: get_assert_output(&assert) --- diff --git a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__basic_config.snap b/crates/cli/tests/snapshots/project_test__basic_config.snap similarity index 89% rename from crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__basic_config.snap rename to crates/cli/tests/snapshots/project_test__basic_config.snap index 2502e4a57b7..14693f41e8b 100644 --- a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__basic_config.snap +++ b/crates/cli/tests/snapshots/project_test__basic_config.snap @@ -1,6 +1,6 @@ --- -source: crates/cli/src/commands/project.rs -assertion_line: 158 +source: crates/cli/tests/project_test.rs +assertion_line: 56 expression: get_assert_output(&assert) --- diff --git a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__depends_on_paths.snap b/crates/cli/tests/snapshots/project_test__depends_on_paths.snap similarity index 91% rename from crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__depends_on_paths.snap rename to crates/cli/tests/snapshots/project_test__depends_on_paths.snap index d7cbba1a95b..23b1aa2d46c 100644 --- a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__depends_on_paths.snap +++ b/crates/cli/tests/snapshots/project_test__depends_on_paths.snap @@ -1,6 +1,6 @@ --- -source: crates/cli/src/commands/project.rs -assertion_line: 184 +source: crates/cli/tests/project_test.rs +assertion_line: 82 expression: get_assert_output(&assert) --- diff --git a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__empty_config.snap b/crates/cli/tests/snapshots/project_test__empty_config.snap similarity index 87% rename from crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__empty_config.snap rename to crates/cli/tests/snapshots/project_test__empty_config.snap index 68fd6d5b81b..5f7241cda31 100644 --- a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__empty_config.snap +++ b/crates/cli/tests/snapshots/project_test__empty_config.snap @@ -1,6 +1,6 @@ --- -source: crates/cli/src/commands/project.rs -assertion_line: 133 +source: crates/cli/tests/project_test.rs +assertion_line: 31 expression: get_assert_output(&assert) --- diff --git a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__no_config.snap b/crates/cli/tests/snapshots/project_test__no_config.snap similarity index 87% rename from crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__no_config.snap rename to crates/cli/tests/snapshots/project_test__no_config.snap index 8d0fdc2f3df..87812046129 100644 --- a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__no_config.snap +++ b/crates/cli/tests/snapshots/project_test__no_config.snap @@ -1,6 +1,6 @@ --- -source: crates/cli/src/commands/project.rs -assertion_line: 145 +source: crates/cli/tests/project_test.rs +assertion_line: 43 expression: get_assert_output(&assert) --- diff --git a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__unknown_project.snap b/crates/cli/tests/snapshots/project_test__unknown_project.snap similarity index 71% rename from crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__unknown_project.snap rename to crates/cli/tests/snapshots/project_test__unknown_project.snap index ec631eaf52a..bbb574f2e89 100644 --- a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__unknown_project.snap +++ b/crates/cli/tests/snapshots/project_test__unknown_project.snap @@ -1,8 +1,7 @@ --- -source: crates/cli/src/commands/project.rs -assertion_line: 113 +source: crates/cli/tests/project_test.rs +assertion_line: 17 expression: get_assert_stderr_output(&assert) - ---  ERROR  No project has been configured with the ID unknown. diff --git a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__with_tasks.snap b/crates/cli/tests/snapshots/project_test__with_tasks.snap similarity index 90% rename from crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__with_tasks.snap rename to crates/cli/tests/snapshots/project_test__with_tasks.snap index 1a326a17261..cdb7827ec15 100644 --- a/crates/cli/src/commands/snapshots/moon_cli__commands__project__tests__with_tasks.snap +++ b/crates/cli/tests/snapshots/project_test__with_tasks.snap @@ -1,6 +1,6 @@ --- -source: crates/cli/src/commands/project.rs -assertion_line: 196 +source: crates/cli/tests/project_test.rs +assertion_line: 94 expression: get_assert_output(&assert) --- diff --git a/crates/cli/tests/snapshots/run_test__caching__uses_cache_on_subsequent_runs-2.snap b/crates/cli/tests/snapshots/run_test__caching__uses_cache_on_subsequent_runs-2.snap new file mode 100644 index 00000000000..b383c170921 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__caching__uses_cache_on_subsequent_runs-2.snap @@ -0,0 +1,15 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 83 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:standard (cached) +stdout + + +Tasks: 1 completed (1 cached) + Time: 100ms + +stderr + + diff --git a/crates/cli/tests/snapshots/run_test__caching__uses_cache_on_subsequent_runs.snap b/crates/cli/tests/snapshots/run_test__caching__uses_cache_on_subsequent_runs.snap new file mode 100644 index 00000000000..d70cfdb325c --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__caching__uses_cache_on_subsequent_runs.snap @@ -0,0 +1,13 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 76 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:standard +stdout + +Tasks: 1 completed + Time: 100ms + +stderr + diff --git a/crates/cli/tests/snapshots/run_test__dependencies__runs_the_graph_in_order.snap b/crates/cli/tests/snapshots/run_test__dependencies__runs_the_graph_in_order.snap new file mode 100644 index 00000000000..6f07d6ef968 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__dependencies__runs_the_graph_in_order.snap @@ -0,0 +1,18 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 48 +expression: get_assert_output(&assert) +--- +▪▪▪▪ depsC:dependencyOrder +deps=c + +▪▪▪▪ depsB:dependencyOrder +deps=b + +▪▪▪▪ depsA:dependencyOrder +deps=a + +Tasks: 3 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__dependencies__runs_the_graph_in_order_not_from_head.snap b/crates/cli/tests/snapshots/run_test__dependencies__runs_the_graph_in_order_not_from_head.snap new file mode 100644 index 00000000000..d7dd18de376 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__dependencies__runs_the_graph_in_order_not_from_head.snap @@ -0,0 +1,15 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 58 +expression: get_assert_output(&assert) +--- +▪▪▪▪ depsC:dependencyOrder +deps=c + +▪▪▪▪ depsB:dependencyOrder +deps=b + +Tasks: 2 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__errors_for_cycle_in_task_deps.snap b/crates/cli/tests/snapshots/run_test__errors_for_cycle_in_task_deps.snap new file mode 100644 index 00000000000..791a29e33b2 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__errors_for_cycle_in_task_deps.snap @@ -0,0 +1,11 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 35 +expression: get_assert_output(&assert) +--- + + ERROR  + +A dependency cycle has been detected for RunTarget(depsA:taskCycle) → RunTarget(depsB:taskCycle) → RunTarget(depsC:taskCycle). + + diff --git a/crates/cli/tests/snapshots/run_test__errors_for_unknown_project.snap b/crates/cli/tests/snapshots/run_test__errors_for_unknown_project.snap new file mode 100644 index 00000000000..c43f1aaa378 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__errors_for_unknown_project.snap @@ -0,0 +1,9 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 14 +expression: get_assert_output(&assert) +--- + + ERROR  No project has been configured with the ID unknown. + + diff --git a/crates/cli/tests/snapshots/run_test__errors_for_unknown_task_in_project.snap b/crates/cli/tests/snapshots/run_test__errors_for_unknown_task_in_project.snap new file mode 100644 index 00000000000..9532ec7e1a0 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__errors_for_unknown_task_in_project.snap @@ -0,0 +1,9 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 24 +expression: get_assert_output(&assert) +--- + + ERROR  Task unknown has not been configured for project base. + + diff --git a/crates/cli/tests/snapshots/run_test__node__engines__adds_engines_constraint.snap b/crates/cli/tests/snapshots/run_test__node__engines__adds_engines_constraint.snap new file mode 100644 index 00000000000..5963f2b0233 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__engines__adds_engines_constraint.snap @@ -0,0 +1,16 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 302 +expression: "read_to_string(fixture.path().join(\"package.json\")).unwrap()" +--- +{ + "name": "test-cases", + "private": true, + "workspaces": [ + "*" + ], + "engines": { + "node": "16.0.0" + } +} + diff --git a/crates/cli/tests/snapshots/run_test__node__engines__doesnt_add_engines_constraint.snap b/crates/cli/tests/snapshots/run_test__node__engines__doesnt_add_engines_constraint.snap new file mode 100644 index 00000000000..e1534f0487e --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__engines__doesnt_add_engines_constraint.snap @@ -0,0 +1,13 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 319 +expression: "read_to_string(fixture.path().join(\"package.json\")).unwrap()" +--- +{ + "name": "test-cases", + "private": true, + "workspaces": [ + "*" + ] +} + diff --git a/crates/cli/tests/snapshots/run_test__node__handles_process_exit_code_nonzero.snap b/crates/cli/tests/snapshots/run_test__node__handles_process_exit_code_nonzero.snap new file mode 100644 index 00000000000..0d599df390f --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__handles_process_exit_code_nonzero.snap @@ -0,0 +1,14 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 352 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:exitCodeNonZero +stdout +This should appear! +stderr +▪▪▪▪ node:exitCodeNonZero + + ERROR  Process ~/.moon/tools/node/16.0.0/bin/node failed with a 1 exit code. + + diff --git a/crates/cli/tests/snapshots/run_test__node__handles_process_exit_code_zero.snap b/crates/cli/tests/snapshots/run_test__node__handles_process_exit_code_zero.snap new file mode 100644 index 00000000000..3dc038343b0 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__handles_process_exit_code_zero.snap @@ -0,0 +1,14 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 44 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:exitCodeZero +stdout +This should appear! + +Tasks: 1 completed + Time: 100ms + +stderr + diff --git a/crates/cli/tests/snapshots/run_test__node__handles_process_exit_nonzero.snap b/crates/cli/tests/snapshots/run_test__node__handles_process_exit_nonzero.snap new file mode 100644 index 00000000000..9214599b353 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__handles_process_exit_nonzero.snap @@ -0,0 +1,13 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 324 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:processExitNonZero +stdout +stderr +▪▪▪▪ node:processExitNonZero + + ERROR  Process ~/.moon/tools/node/16.0.0/bin/node failed with a 1 exit code. + + diff --git a/crates/cli/tests/snapshots/run_test__node__handles_process_exit_zero.snap b/crates/cli/tests/snapshots/run_test__node__handles_process_exit_zero.snap new file mode 100644 index 00000000000..1a7793bae26 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__handles_process_exit_zero.snap @@ -0,0 +1,13 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 24 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:processExitZero +stdout + +Tasks: 1 completed + Time: 100ms + +stderr + diff --git a/crates/cli/tests/snapshots/run_test__node__handles_unhandled_promise.snap b/crates/cli/tests/snapshots/run_test__node__handles_unhandled_promise.snap new file mode 100644 index 00000000000..3a6bd4c3860 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__handles_unhandled_promise.snap @@ -0,0 +1,20 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 382 +expression: "get_path_safe_output(&assert, fixture.path())" +--- +▪▪▪▪ node:unhandledPromise +stdout +stderr +node:internal/process/promises:246 + triggerUncaughtException(err, true /* fromPromise */); + ^ + +[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "Oops".] { + code: 'ERR_UNHANDLED_REJECTION' +} +▪▪▪▪ node:unhandledPromise + + ERROR  Process ~/.moon/tools/node/16.0.0/bin/node failed with a 1 exit code. + + diff --git a/crates/cli/tests/snapshots/run_test__node__inherits_moon_env_vars.snap b/crates/cli/tests/snapshots/run_test__node__inherits_moon_env_vars.snap new file mode 100644 index 00000000000..b7630bb1831 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__inherits_moon_env_vars.snap @@ -0,0 +1,21 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 360 +expression: "get_path_safe_output(&assert, fixture.path())" +--- +▪▪▪▪ node:envVarsMoon +MOON_CACHE=write +MOON_CACHE_DIR=/.moon/cache +MOON_PROJECT_ID=node +MOON_PROJECT_ROOT=/node +MOON_PROJECT_RUNFILE=/.moon/cache/runs/node/runfile.json +MOON_PROJECT_SOURCE=node +MOON_RUN_TARGET=node:envVarsMoon +MOON_TOOLCHAIN_DIR=~/.moon +MOON_WORKING_DIR= +MOON_WORKSPACE_ROOT= + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__node__passes_args_through.snap b/crates/cli/tests/snapshots/run_test__node__passes_args_through.snap new file mode 100644 index 00000000000..038dc62cbf8 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__passes_args_through.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 163 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:passthroughArgs +-aBc --opt value --optCamel=value foo 'bar baz' --opt-kebab 123 + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__node__runs_cjs_files.snap b/crates/cli/tests/snapshots/run_test__node__runs_cjs_files.snap new file mode 100644 index 00000000000..175a3806780 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__runs_cjs_files.snap @@ -0,0 +1,13 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 34 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:cjs +stdout + +Tasks: 1 completed + Time: 100ms + +stderr + diff --git a/crates/cli/tests/snapshots/run_test__node__runs_from_project_root.snap b/crates/cli/tests/snapshots/run_test__node__runs_from_project_root.snap new file mode 100644 index 00000000000..54c1166eac7 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__runs_from_project_root.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 372 +expression: "get_path_safe_output(&assert, fixture.path())" +--- +▪▪▪▪ node:runFromProject +/node + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__node__runs_from_workspace_root.snap b/crates/cli/tests/snapshots/run_test__node__runs_from_workspace_root.snap new file mode 100644 index 00000000000..ae42aba8f69 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__runs_from_workspace_root.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 384 +expression: "get_path_safe_output(&assert, fixture.path())" +--- +▪▪▪▪ node:runFromWorkspace + + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__node__runs_mjs_files.snap b/crates/cli/tests/snapshots/run_test__node__runs_mjs_files.snap new file mode 100644 index 00000000000..0f43fe5711d --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__runs_mjs_files.snap @@ -0,0 +1,13 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 44 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:mjs +stdout + +Tasks: 1 completed + Time: 100ms + +stderr + diff --git a/crates/cli/tests/snapshots/run_test__node__runs_package_managers.snap b/crates/cli/tests/snapshots/run_test__node__runs_package_managers.snap new file mode 100644 index 00000000000..97624c533fa --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__runs_package_managers.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 44 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:npm +latest + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__node__runs_standard_script.snap b/crates/cli/tests/snapshots/run_test__node__runs_standard_script.snap new file mode 100644 index 00000000000..1bba4c6a899 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__runs_standard_script.snap @@ -0,0 +1,13 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 14 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:standard +stdout + +Tasks: 1 completed + Time: 100ms + +stderr + diff --git a/crates/cli/tests/snapshots/run_test__node__sets_env_vars.snap b/crates/cli/tests/snapshots/run_test__node__sets_env_vars.snap new file mode 100644 index 00000000000..f873c2d439f --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__sets_env_vars.snap @@ -0,0 +1,14 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 348 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:envVars +MOON_FOO=abc +MOON_BAR=123 +MOON_BAZ=true + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__node__supports_top_level_await.snap b/crates/cli/tests/snapshots/run_test__node__supports_top_level_await.snap new file mode 100644 index 00000000000..995305dfbdf --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__supports_top_level_await.snap @@ -0,0 +1,14 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 54 +expression: get_assert_output(&assert) +--- +▪▪▪▪ node:topLevelAwait +before +awaiting +after + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__node__sync_depends_on__syncs_as_dependency_to_package_json.snap b/crates/cli/tests/snapshots/run_test__node__sync_depends_on__syncs_as_dependency_to_package_json.snap new file mode 100644 index 00000000000..9ec621babe8 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__sync_depends_on__syncs_as_dependency_to_package_json.snap @@ -0,0 +1,14 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 531 +expression: "read_to_string(fixture.path().join(\"depends-on/package.json\")).unwrap()" +--- +{ + "name": "test-cases-depends-on", + "dependencies": { + "react": "17.0.0", + "test-cases-deps-a": "*", + "test-cases-deps-b": "*" + } +} + diff --git a/crates/cli/tests/snapshots/run_test__node__sync_depends_on__syncs_as_reference_to_tsconfig_json-2.snap b/crates/cli/tests/snapshots/run_test__node__sync_depends_on__syncs_as_reference_to_tsconfig_json-2.snap new file mode 100644 index 00000000000..5ed0d42275c --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__sync_depends_on__syncs_as_reference_to_tsconfig_json-2.snap @@ -0,0 +1,19 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 555 +expression: "read_to_string(fixture.path().join(\"depends-on/tsconfig.json\")).unwrap()" +--- +{ + "exclude": [ + "*.js" + ], + "references": [ + { + "path": "../deps-b" + }, + { + "path": "../deps-c" + } + ] +} + diff --git a/crates/cli/tests/snapshots/run_test__node__sync_depends_on__syncs_as_reference_to_tsconfig_json.snap b/crates/cli/tests/snapshots/run_test__node__sync_depends_on__syncs_as_reference_to_tsconfig_json.snap new file mode 100644 index 00000000000..9ade906ca4c --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__sync_depends_on__syncs_as_reference_to_tsconfig_json.snap @@ -0,0 +1,22 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 681 +expression: "read_to_string(fixture.path().join(\"tsconfig.json\")).unwrap()" +--- +{ + "references": [ + { + "path": "base" + }, + { + "path": "depends-on" + }, + { + "path": "deps-b" + }, + { + "path": "deps-c" + } + ] +} + diff --git a/crates/cli/tests/snapshots/run_test__node__version_manager__errors_for_invalid_value.snap b/crates/cli/tests/snapshots/run_test__node__version_manager__errors_for_invalid_value.snap new file mode 100644 index 00000000000..82393d71328 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node__version_manager__errors_for_invalid_value.snap @@ -0,0 +1,13 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 387 +expression: get_assert_output(&assert) +--- + + ERROR  + +Failed to validate .moon/workspace.yml configuration file. + +node.syncVersionManagerConfig: Unknown option `invalid`. + + diff --git a/crates/cli/tests/snapshots/run_test__node_npm__installs_correct_version.snap b/crates/cli/tests/snapshots/run_test__node_npm__installs_correct_version.snap new file mode 100644 index 00000000000..23f5a77dbde --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node_npm__installs_correct_version.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 281 +expression: get_assert_output(&assert) +--- +▪▪▪▪ npm:version +7.0.0 + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__node_pnpm__installs_correct_version.snap b/crates/cli/tests/snapshots/run_test__node_pnpm__installs_correct_version.snap new file mode 100644 index 00000000000..39f45abe3af --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node_pnpm__installs_correct_version.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 337 +expression: get_assert_output(&assert) +--- +▪▪▪▪ pnpm:version +6.32.0 + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__node_yarn1__installs_correct_version.snap b/crates/cli/tests/snapshots/run_test__node_yarn1__installs_correct_version.snap new file mode 100644 index 00000000000..fb0c310381c --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__node_yarn1__installs_correct_version.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 383 +expression: get_assert_output(&assert) +--- +▪▪▪▪ yarn:version +1.22.0 + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system__handles_echo.snap b/crates/cli/tests/snapshots/run_test__system__handles_echo.snap new file mode 100644 index 00000000000..cafb637965a --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system__handles_echo.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 128 +expression: get_assert_output(&assert) +--- +▪▪▪▪ system:echo +hello + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system__handles_ls.snap b/crates/cli/tests/snapshots/run_test__system__handles_ls.snap new file mode 100644 index 00000000000..aed82614ab0 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system__handles_ls.snap @@ -0,0 +1,19 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 808 +expression: get_assert_output(&assert) +--- +▪▪▪▪ system:ls +cwd.sh +envVars.sh +envVarsMoon.sh +exitNonZero.sh +exitZero.sh +passthroughArgs.sh +project.yml +standard.sh + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system__handles_ls_from_root.snap b/crates/cli/tests/snapshots/run_test__system__handles_ls_from_root.snap new file mode 100644 index 00000000000..8247060df80 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system__handles_ls_from_root.snap @@ -0,0 +1,25 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 714 +expression: get_assert_output(&assert) +--- +▪▪▪▪ system:lsRoot +base +depends-on +deps-a +deps-b +deps-c +node +node_modules +package-lock.json +package.json +system +target-scope-a +target-scope-b +target-scope-c +tsconfig.json + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system__handles_process_exit_nonzero.snap b/crates/cli/tests/snapshots/run_test__system__handles_process_exit_nonzero.snap new file mode 100644 index 00000000000..65a8b189864 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system__handles_process_exit_nonzero.snap @@ -0,0 +1,13 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 844 +expression: get_assert_output(&assert) +--- +▪▪▪▪ system:exitNonZero +stdout +stderr +▪▪▪▪ system:exitNonZero + + ERROR  Process bash failed with a 1 exit code. + + diff --git a/crates/cli/tests/snapshots/run_test__system__handles_process_exit_zero.snap b/crates/cli/tests/snapshots/run_test__system__handles_process_exit_zero.snap new file mode 100644 index 00000000000..ab287b40be6 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system__handles_process_exit_zero.snap @@ -0,0 +1,13 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 762 +expression: get_assert_output(&assert) +--- +▪▪▪▪ system:exitZero +stdout + +Tasks: 1 completed + Time: 100ms + +stderr + diff --git a/crates/cli/tests/snapshots/run_test__system__inherits_moon_env_vars.snap b/crates/cli/tests/snapshots/run_test__system__inherits_moon_env_vars.snap new file mode 100644 index 00000000000..039b4336025 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system__inherits_moon_env_vars.snap @@ -0,0 +1,21 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 771 +expression: "get_path_safe_output(&assert, fixture.path())" +--- +▪▪▪▪ system:envVarsMoon +MOON_CACHE=write +MOON_CACHE_DIR=/.moon/cache +MOON_PROJECT_ID=system +MOON_PROJECT_ROOT=/system +MOON_PROJECT_RUNFILE=/.moon/cache/runs/system/runfile.json +MOON_PROJECT_SOURCE=system +MOON_RUN_TARGET=system:envVarsMoon +MOON_TOOLCHAIN_DIR=~/.moon +MOON_WORKING_DIR= +MOON_WORKSPACE_ROOT= + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system__passes_args_through.snap b/crates/cli/tests/snapshots/run_test__system__passes_args_through.snap new file mode 100644 index 00000000000..073db9f38a4 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system__passes_args_through.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 216 +expression: get_assert_output(&assert) +--- +▪▪▪▪ system:passthroughArgs +-aBc --opt value --optCamel=value foo 'bar baz' --opt-kebab 123 + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system__retries_on_failure_till_count.snap b/crates/cli/tests/snapshots/run_test__system__retries_on_failure_till_count.snap new file mode 100644 index 00000000000..96adc05ae6e --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system__retries_on_failure_till_count.snap @@ -0,0 +1,25 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 925 +expression: get_assert_output(&assert) +--- +▪▪▪▪ system:retryCount +stdout +▪▪▪▪ system:retryCount (attempt 2 of 4) +stdout +▪▪▪▪ system:retryCount (attempt 3 of 4) +stdout +▪▪▪▪ system:retryCount (attempt 4 of 4) +stdout +stderr +▪▪▪▪ system:retryCount +stderr +▪▪▪▪ system:retryCount (attempt 2 of 4) +stderr +▪▪▪▪ system:retryCount (attempt 3 of 4) +stderr +▪▪▪▪ system:retryCount (attempt 4 of 4) + + ERROR  Process bash failed with a 1 exit code. + + diff --git a/crates/cli/tests/snapshots/run_test__system__runs_bash_script.snap b/crates/cli/tests/snapshots/run_test__system__runs_bash_script.snap new file mode 100644 index 00000000000..12f462f9207 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system__runs_bash_script.snap @@ -0,0 +1,13 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 148 +expression: get_assert_output(&assert) +--- +▪▪▪▪ system:bash +stdout + +Tasks: 1 completed + Time: 100ms + +stderr + diff --git a/crates/cli/tests/snapshots/run_test__system__runs_from_project_root.snap b/crates/cli/tests/snapshots/run_test__system__runs_from_project_root.snap new file mode 100644 index 00000000000..9a164b652b4 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system__runs_from_project_root.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 795 +expression: "get_path_safe_output(&assert, fixture.path())" +--- +▪▪▪▪ system:runFromProject +/system + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system__runs_from_workspace_root.snap b/crates/cli/tests/snapshots/run_test__system__runs_from_workspace_root.snap new file mode 100644 index 00000000000..a72dc9c30e8 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system__runs_from_workspace_root.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 807 +expression: "get_path_safe_output(&assert, fixture.path())" +--- +▪▪▪▪ system:runFromWorkspace + + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system__sets_env_vars.snap b/crates/cli/tests/snapshots/run_test__system__sets_env_vars.snap new file mode 100644 index 00000000000..c3d9b25f96c --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system__sets_env_vars.snap @@ -0,0 +1,14 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 759 +expression: get_assert_output(&assert) +--- +▪▪▪▪ system:envVars +MOON_FOO=abc +MOON_BAR=123 +MOON_BAZ=true + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system_windows__handles_process_exit_nonzero.snap b/crates/cli/tests/snapshots/run_test__system_windows__handles_process_exit_nonzero.snap new file mode 100644 index 00000000000..f8065053076 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system_windows__handles_process_exit_nonzero.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +expression: get_assert_output(&assert) +--- +▪▪▪▪ systemWindows:exitNonZero +stdout +stderr +▪▪▪▪ systemWindows:exitNonZero + + ERROR  Process cmd.exe failed with a 1 exit code. + + diff --git a/crates/cli/tests/snapshots/run_test__system_windows__handles_process_exit_zero.snap b/crates/cli/tests/snapshots/run_test__system_windows__handles_process_exit_zero.snap new file mode 100644 index 00000000000..658c687456b --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system_windows__handles_process_exit_zero.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +expression: get_assert_output(&assert) +--- +▪▪▪▪ systemWindows:exitZero +stdout + +Tasks: 1 completed + Time: 100ms + +stderr + diff --git a/crates/cli/tests/snapshots/run_test__system_windows__inherits_moon_env_vars.snap b/crates/cli/tests/snapshots/run_test__system_windows__inherits_moon_env_vars.snap new file mode 100644 index 00000000000..071046aea7a --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system_windows__inherits_moon_env_vars.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +expression: "get_path_safe_output(&assert, fixture.path())" +--- +▪▪▪▪ systemWindows:envVarsMoon +MOON_RUN_TARGET=systemWindows:envVarsMoon +MOON_PROJECT_ID=systemWindows + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system_windows__passes_args_through.snap b/crates/cli/tests/snapshots/run_test__system_windows__passes_args_through.snap new file mode 100644 index 00000000000..95661c10e9e --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system_windows__passes_args_through.snap @@ -0,0 +1,11 @@ +--- +source: crates/cli/tests/run_test.rs +expression: get_assert_output(&assert) +--- +▪▪▪▪ systemWindows:passthroughArgs +-aBc --opt value --optCamel=value foo "'bar baz'" --opt-kebab 123 + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system_windows__retries_on_failure_till_count.snap b/crates/cli/tests/snapshots/run_test__system_windows__retries_on_failure_till_count.snap new file mode 100644 index 00000000000..081c7951600 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system_windows__retries_on_failure_till_count.snap @@ -0,0 +1,24 @@ +--- +source: crates/cli/tests/run_test.rs +expression: get_assert_output(&assert) +--- +▪▪▪▪ systemWindows:retryCount +stdout +▪▪▪▪ systemWindows:retryCount (attempt 2 of 4) +stdout +▪▪▪▪ systemWindows:retryCount (attempt 3 of 4) +stdout +▪▪▪▪ systemWindows:retryCount (attempt 4 of 4) +stdout +stderr +▪▪▪▪ systemWindows:retryCount +stderr +▪▪▪▪ systemWindows:retryCount (attempt 2 of 4) +stderr +▪▪▪▪ systemWindows:retryCount (attempt 3 of 4) +stderr +▪▪▪▪ systemWindows:retryCount (attempt 4 of 4) + + ERROR  Process cmd.exe failed with a 1 exit code. + + diff --git a/crates/cli/tests/snapshots/run_test__system_windows__runs_bat_script.snap b/crates/cli/tests/snapshots/run_test__system_windows__runs_bat_script.snap new file mode 100644 index 00000000000..821c56f4a88 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system_windows__runs_bat_script.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_test.rs +expression: "get_path_safe_output(&assert, fixture.path())" +--- +▪▪▪▪ systemWindows:bat +stdout + +Tasks: 1 completed + Time: 100ms + +stderr + diff --git a/crates/cli/tests/snapshots/run_test__system_windows__runs_from_project_root.snap b/crates/cli/tests/snapshots/run_test__system_windows__runs_from_project_root.snap new file mode 100644 index 00000000000..89d2c05f973 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system_windows__runs_from_project_root.snap @@ -0,0 +1,11 @@ +--- +source: crates/cli/tests/run_test.rs +expression: "get_path_safe_output(&assert, fixture.path())" +--- +▪▪▪▪ systemWindows:runFromProject +\system-windows + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system_windows__runs_from_workspace_root.snap b/crates/cli/tests/snapshots/run_test__system_windows__runs_from_workspace_root.snap new file mode 100644 index 00000000000..96c6438c6d5 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system_windows__runs_from_workspace_root.snap @@ -0,0 +1,11 @@ +--- +source: crates/cli/tests/run_test.rs +expression: "get_path_safe_output(&assert, fixture.path())" +--- +▪▪▪▪ systemWindows:runFromWorkspace + + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__system_windows__sets_env_vars.snap b/crates/cli/tests/snapshots/run_test__system_windows__sets_env_vars.snap new file mode 100644 index 00000000000..69c86aa4559 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__system_windows__sets_env_vars.snap @@ -0,0 +1,13 @@ +--- +source: crates/cli/tests/run_test.rs +expression: get_assert_output(&assert) +--- +▪▪▪▪ systemWindows:envVars +MOON_FOO=abc +MOON_BAR=123 +MOON_BAZ=true + +Tasks: 1 completed + Time: 100ms + + diff --git a/crates/cli/tests/snapshots/run_test__target_scopes__errors_for_deps_scope.snap b/crates/cli/tests/snapshots/run_test__target_scopes__errors_for_deps_scope.snap new file mode 100644 index 00000000000..593dde8e753 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__target_scopes__errors_for_deps_scope.snap @@ -0,0 +1,9 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 38 +expression: get_assert_output(&assert) +--- + + ERROR  Project dependencies scope (^:) is not supported in run contexts. + + diff --git a/crates/cli/tests/snapshots/run_test__target_scopes__errors_for_self_scope.snap b/crates/cli/tests/snapshots/run_test__target_scopes__errors_for_self_scope.snap new file mode 100644 index 00000000000..552e5fe2a78 --- /dev/null +++ b/crates/cli/tests/snapshots/run_test__target_scopes__errors_for_self_scope.snap @@ -0,0 +1,9 @@ +--- +source: crates/cli/tests/run_test.rs +assertion_line: 48 +expression: get_assert_output(&assert) +--- + + ERROR  Project self scope (~:) is not supported in run contexts. + + diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 25453621e19..47c9fae7328 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -13,8 +13,8 @@ pub use project::{ProjectConfig, ProjectMetadataConfig, ProjectType}; pub use types::{FilePath, FilePathOrGlob, ProjectID, TargetID, TaskID}; pub use validator::ValidationErrors; pub use workspace::{ - NodeConfig, NpmConfig, PackageManager, PnpmConfig, VcsConfig, VcsManager, WorkspaceConfig, - YarnConfig, + NodeConfig, NpmConfig, PackageManager, PnpmConfig, TypeScriptConfig, VcsConfig, VcsManager, + WorkspaceConfig, YarnConfig, }; pub fn load_workspace_config_template() -> &'static str { diff --git a/crates/config/src/package.rs b/crates/config/src/package.rs index e5947e60812..14dbd018c73 100644 --- a/crates/config/src/package.rs +++ b/crates/config/src/package.rs @@ -164,9 +164,10 @@ impl PackageJson { Ok(cfg) } - pub async fn save(&self) -> Result<(), MoonError> { + pub async fn save(&mut self) -> Result<(), MoonError> { if self.dirty { write_preserved_json(&self.path, self).await?; + self.dirty = false; } Ok(()) @@ -439,7 +440,7 @@ mod test { let file = dir.child("package.json"); file.write_str(json).unwrap(); - let package = PackageJson::load(file.path()).await.unwrap(); + let mut package = PackageJson::load(file.path()).await.unwrap(); package.save().await.unwrap(); assert_eq!(fs::read_json_string(file.path()).await.unwrap(), json,); diff --git a/crates/config/src/project/task.rs b/crates/config/src/project/task.rs index aa52038400b..cb706fdcf1b 100644 --- a/crates/config/src/project/task.rs +++ b/crates/config/src/project/task.rs @@ -40,7 +40,7 @@ fn validate_outputs(list: &[String]) -> Result<(), ValidationError> { #[serde(rename_all = "lowercase")] pub enum TaskType { Node, - Shell, + System, } impl Default for TaskType { diff --git a/crates/config/src/tsconfig.rs b/crates/config/src/tsconfig.rs index e6499a6d994..a87b8694b61 100644 --- a/crates/config/src/tsconfig.rs +++ b/crates/config/src/tsconfig.rs @@ -2,7 +2,7 @@ use json; use moon_error::{map_io_to_fs_error, map_json_to_error, MoonError}; -use moon_utils::fs; +use moon_utils::{fs, path::standardize_separators}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use std::collections::BTreeMap; @@ -82,11 +82,11 @@ impl TsConfigJson { /// path and tsconfig file name, and sort the list based on path. /// Return true if the new value is different from the old value. pub fn add_project_ref(&mut self, base_path: &str, tsconfig_name: &str) -> bool { + let mut path = standardize_separators(base_path); + // File name is optional when using standard naming - let path = if tsconfig_name == "tsconfig.json" { - base_path.to_owned() - } else { - format!("{}/{}", base_path, tsconfig_name) + if tsconfig_name != "tsconfig.json" { + path = format!("{}/{}", path, tsconfig_name) }; let mut references = match &self.references { @@ -116,9 +116,10 @@ impl TsConfigJson { true } - pub async fn save(&self) -> Result<(), MoonError> { + pub async fn save(&mut self) -> Result<(), MoonError> { if self.dirty { write_preserved_json(&self.path, self).await?; + self.dirty = false; } Ok(()) diff --git a/crates/config/src/workspace/mod.rs b/crates/config/src/workspace/mod.rs index 3a82c597d1b..7d39604dfac 100644 --- a/crates/config/src/workspace/mod.rs +++ b/crates/config/src/workspace/mod.rs @@ -41,7 +41,7 @@ fn validate_projects(projects: &HashMap) -> Result<(), Validat } /// https://moonrepo.dev/docs/config/workspace -#[derive(Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize, Validate)] +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize, Validate)] pub struct WorkspaceConfig { #[serde(default)] #[validate] diff --git a/crates/config/src/workspace/node.rs b/crates/config/src/workspace/node.rs index 01f11328d1e..1f6bfaddb90 100644 --- a/crates/config/src/workspace/node.rs +++ b/crates/config/src/workspace/node.rs @@ -62,14 +62,14 @@ impl Default for PackageManager { #[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] pub enum VersionManager { - NodeEnv, + Nodenv, Nvm, } impl VersionManager { pub fn get_config_file_name(&self) -> String { match self { - VersionManager::NodeEnv => String::from(".node-version"), + VersionManager::Nodenv => String::from(".node-version"), VersionManager::Nvm => String::from(".nvmrc"), } } diff --git a/crates/config/templates/workspace.yml b/crates/config/templates/workspace.yml index 880a70aad9e..0aac5fc8f73 100644 --- a/crates/config/templates/workspace.yml +++ b/crates/config/templates/workspace.yml @@ -13,7 +13,7 @@ projects: node: # The version to use. Must be a semantic version that includes major, minor, and patch. # We suggest using the latest active LTS version: https://nodejs.org/en/about/releases - version: '16.13.0' + version: '18.0.0' # OPTIONAL: The package manager to use when managing dependencies. # Accepts "npm", "pnpm", or "yarn". Defaults to "npm". @@ -30,7 +30,7 @@ node: syncProjectWorkspaceDependencies: true # OPTIONAL: Sync `node.version` to a 3rd-party version manager's config file. - # Accepts "nodeenv" (.node-version), "nvm" (.nvmrc), or none. + # Accepts "nodenv" (.node-version), "nvm" (.nvmrc), or none. # syncVersionManagerConfig: 'nvm' # OPTIONAL: Configures how Moon integrates with TypeScript. diff --git a/crates/error/src/lib.rs b/crates/error/src/lib.rs index 97097c8a02b..09d36a05841 100644 --- a/crates/error/src/lib.rs +++ b/crates/error/src/lib.rs @@ -27,11 +27,14 @@ pub enum MoonError { #[error("Path {0} contains invalid UTF-8 characters.")] PathInvalidUTF8(PathBuf), - #[error("Process failure for {0}: {1}")] + #[error("Process failure for {0}: {1}")] Process(String, #[source] IoError), - #[error("Process {0} failed with a {1} exit code. {2}")] - ProcessNonZero(String, i32, String), + #[error("Process {0} failed with a {1} exit code.")] + ProcessNonZero(String, i32), + + #[error("Process {0} failed with a {1} exit code.\n{2}")] + ProcessNonZeroWithOutput(String, i32, String), #[error("{0}")] Unknown(#[source] IoError), diff --git a/crates/logger/Cargo.toml b/crates/logger/Cargo.toml index 13402d1e2ec..6e9f1d7bd89 100644 --- a/crates/logger/Cargo.toml +++ b/crates/logger/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" [dependencies] chrono = "0.4" console = "0.15" +dirs = "4.0" fern = "0.6" log = "0.4" diff --git a/crates/logger/src/color.rs b/crates/logger/src/color.rs index d361f1e9e6f..176d11dbee4 100644 --- a/crates/logger/src/color.rs +++ b/crates/logger/src/color.rs @@ -3,6 +3,7 @@ pub use console::style; use console::{colors_enabled, pad_str, Alignment}; +use dirs::home_dir as get_home_dir; use log::Level; use std::env; use std::path::Path; @@ -56,7 +57,10 @@ pub fn file(path: &str) -> String { } pub fn path(path: &Path) -> String { - paint(Color::Cyan as u8, path.to_str().unwrap_or("")) + paint( + Color::Cyan as u8, + &clean_path(path.to_str().unwrap_or("")), + ) } pub fn url(url: &str) -> String { @@ -64,7 +68,7 @@ pub fn url(url: &str) -> String { } pub fn shell(cmd: &str) -> String { - paint(Color::Pink as u8, cmd) + paint(Color::Pink as u8, &clean_path(cmd)) } pub fn symbol(value: &str) -> String { @@ -166,3 +170,21 @@ pub const COLOR_LIST: [u8; 76] = [ ]; pub const COLOR_LIST_UNSUPPORTED: [u8; 6] = [6, 2, 3, 4, 5, 1]; + +fn clean_path(path: &str) -> String { + let mut path_str = path.to_owned(); + + if let Some(home_dir) = get_home_dir() { + let home_dir_str = home_dir.to_str().unwrap(); + + if path.starts_with(&home_dir_str) { + path_str = path_str.replace(home_dir.to_str().unwrap(), "~"); + } + } + + if env::var("MOON_TEST_STANDARDIZE_PATHS").is_ok() { + path_str = path_str.replace('\\', "/"); + } + + path_str +} diff --git a/crates/logger/src/lib.rs b/crates/logger/src/lib.rs index ab2c8c6ead1..d68c435d9e3 100644 --- a/crates/logger/src/lib.rs +++ b/crates/logger/src/lib.rs @@ -10,3 +10,8 @@ pub use log::{debug, error, info, max_level, trace, warn, LevelFilter}; pub fn logging_enabled() -> bool { max_level() != LevelFilter::Off } + +pub trait Logable { + /// Return a unique name for logging. + fn get_log_target(&self) -> String; +} diff --git a/crates/project/src/file_group.rs b/crates/project/src/file_group.rs index 333e7bf5550..f6c8f40f328 100644 --- a/crates/project/src/file_group.rs +++ b/crates/project/src/file_group.rs @@ -1,7 +1,7 @@ use crate::errors::{ProjectError, TokenError}; use common_path::common_path_all; use globwalk::GlobWalkerBuilder; -use moon_utils::fs::{expand_root_path, is_glob}; +use moon_utils::path::{expand_root_path, is_glob}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 89a74c5e2db..b4cd18289fe 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -9,7 +9,7 @@ use moon_config::package::PackageJson; use moon_config::tsconfig::TsConfigJson; use moon_config::{FilePath, GlobalProjectConfig, ProjectConfig, ProjectID, TaskID}; use moon_logger::{color, debug, trace}; -use moon_utils::fs; +use moon_utils::path; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; @@ -202,7 +202,7 @@ impl Project { workspace_root: &Path, global_config: &GlobalProjectConfig, ) -> Result { - let root = workspace_root.join(&fs::normalize_separators(source)); + let root = workspace_root.join(&path::normalize_separators(source)); debug!( target: &format!("moon:project:{}", id), diff --git a/crates/project/src/task.rs b/crates/project/src/task.rs index a577e845f80..bc81276b1e0 100644 --- a/crates/project/src/task.rs +++ b/crates/project/src/task.rs @@ -7,7 +7,7 @@ use moon_config::{ FilePath, FilePathOrGlob, TargetID, TaskConfig, TaskMergeStrategy, TaskOptionsConfig, TaskType, }; use moon_logger::{color, debug, trace}; -use moon_utils::fs; +use moon_utils::{fs, path}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -265,10 +265,10 @@ impl Task { for input in &token_resolver.resolve(&self.inputs, None)? { // We cant canonicalize here as these inputs may not exist! - if fs::is_path_glob(input) { - self.input_globs.push(fs::normalize_glob(input)); + if path::is_path_glob(input) { + self.input_globs.push(path::normalize_glob(input)); } else { - self.input_paths.insert(fs::normalize(input)); + self.input_paths.insert(path::normalize(input)); } } @@ -288,13 +288,13 @@ impl Task { ); for output in &token_resolver.resolve(&self.outputs, None)? { - if fs::is_path_glob(output) { + if path::is_path_glob(output) { return Err(ProjectError::NoOutputGlob( output.to_owned(), self.target.clone(), )); } else { - self.output_paths.insert(fs::normalize(output)); + self.output_paths.insert(path::normalize(output)); } } diff --git a/crates/project/src/token.rs b/crates/project/src/token.rs index d85a98afea4..dfa01c418f3 100644 --- a/crates/project/src/token.rs +++ b/crates/project/src/token.rs @@ -3,7 +3,7 @@ use crate::file_group::FileGroup; use crate::target::Target; use crate::task::Task; use moon_logger::{color, trace, warn}; -use moon_utils::fs::{expand_root_path, is_glob}; +use moon_utils::path::{expand_root_path, is_glob}; use moon_utils::regex::{ matches_token_func, matches_token_var, TOKEN_FUNC_ANYWHERE_PATTERN, TOKEN_FUNC_PATTERN, TOKEN_VAR_PATTERN, diff --git a/crates/project/tests/project_test.rs b/crates/project/tests/project_test.rs index 9808e9a1ef4..fa562bb829a 100644 --- a/crates/project/tests/project_test.rs +++ b/crates/project/tests/project_test.rs @@ -7,7 +7,6 @@ use moon_project::{EnvVars, FileGroup, Project, ProjectError, Target, Task}; use moon_utils::string_vec; use moon_utils::test::{get_fixtures_dir, get_fixtures_root}; use std::collections::{BTreeMap, HashMap}; -use std::env; use std::path::Path; fn mock_file_groups() -> HashMap { @@ -433,7 +432,7 @@ mod tasks { inputs: Some(string_vec!["b.*"]), outputs: Some(string_vec!["b.ts"]), options: mock_local_task_options_config(TaskMergeStrategy::Replace), - type_of: TaskType::Shell, + type_of: TaskType::System, } )]), ..ProjectConfig::default() @@ -453,7 +452,7 @@ mod tasks { inputs: Some(string_vec!["b.*"]), outputs: Some(string_vec!["b.ts"]), options: mock_merged_task_options_config(TaskMergeStrategy::Replace), - type_of: TaskType::Shell, + type_of: TaskType::System, }, &workspace_root, project_source @@ -507,7 +506,7 @@ mod tasks { inputs: Some(string_vec!["b.*"]), outputs: Some(string_vec!["b.ts"]), options: mock_local_task_options_config(TaskMergeStrategy::Append), - type_of: TaskType::Shell, + type_of: TaskType::System, } )]), ..ProjectConfig::default() @@ -530,7 +529,7 @@ mod tasks { inputs: Some(string_vec!["a.*", "b.*"]), outputs: Some(string_vec!["a.ts", "b.ts"]), options: mock_merged_task_options_config(TaskMergeStrategy::Append), - type_of: TaskType::Shell, + type_of: TaskType::System, }, &workspace_root, project_source @@ -584,7 +583,7 @@ mod tasks { inputs: Some(string_vec!["b.*"]), outputs: Some(string_vec!["b.ts"]), options: mock_local_task_options_config(TaskMergeStrategy::Prepend), - type_of: TaskType::Shell, + type_of: TaskType::System, } )]), ..ProjectConfig::default() @@ -607,7 +606,7 @@ mod tasks { inputs: Some(string_vec!["b.*", "a.*"]), outputs: Some(string_vec!["b.ts", "a.ts"]), options: mock_merged_task_options_config(TaskMergeStrategy::Prepend), - type_of: TaskType::Shell, + type_of: TaskType::System, }, &workspace_root, project_source @@ -829,7 +828,7 @@ mod tasks { assert_eq!( *project.tasks.get("test").unwrap().args, - if env::consts::OS == "windows" { + if cfg!(windows) { vec![ "--dirs", ".\\dir", @@ -964,7 +963,7 @@ mod tasks { project_root.to_str().unwrap(), "--psource", // This is wonky but also still valid - if env::consts::OS == "windows" { + if cfg!(windows) { "foo/base\\files-and-dirs" } else { "foo/base/files-and-dirs" diff --git a/crates/toolchain/src/errors.rs b/crates/toolchain/src/errors.rs index 61e9b90be0d..27e80dd00e3 100644 --- a/crates/toolchain/src/errors.rs +++ b/crates/toolchain/src/errors.rs @@ -3,6 +3,9 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum ToolchainError { + #[error("Internet connection required, unable to download and install tools.")] + InternetConnectionRequired, + #[error( "Shashum check has failed for {0}, which was downloaded from {1}." )] diff --git a/crates/toolchain/src/helpers.rs b/crates/toolchain/src/helpers.rs index 880e771f8e9..735cfcded91 100644 --- a/crates/toolchain/src/helpers.rs +++ b/crates/toolchain/src/helpers.rs @@ -3,23 +3,33 @@ use flate2::read::GzDecoder; use moon_error::map_io_to_fs_error; use moon_logger::{color, trace}; use moon_utils::fs; -use moon_utils::process::{create_command, exec_command_capture_stdout}; +use moon_utils::process::{output_to_trimmed_string, Command}; use sha2::{Digest, Sha256}; use std::env; use std::fs::File; use std::io; -use std::path::{Path, PathBuf}; +use std::path::Path; use tar::Archive; use zip::ZipArchive; +pub fn get_bin_name_suffix(name: &str, windows_ext: &str, flat: bool) -> String { + if cfg!(windows) { + format!("{}.{}", name, windows_ext) + } else if flat { + name.to_owned() + } else { + format!("bin/{}", name) + } +} + pub async fn get_bin_version(bin: &Path) -> Result { - let mut version = exec_command_capture_stdout(create_command(bin).args(["--version"]).env( - "PATH", - get_path_env_var(bin.parent().unwrap().to_path_buf()), - )) - .await?; + let output = Command::new(bin) + .arg("--version") + .env("PATH", get_path_env_var(bin.parent().unwrap())) + .exec_capture_output() + .await?; - version = version.trim().to_owned(); + let mut version = output_to_trimmed_string(&output.stdout); if version.is_empty() { version = String::from("0.0.0"); @@ -56,9 +66,9 @@ pub fn get_file_sha256_hash(path: &Path) -> Result { /// other binaries of the same name. Otherwise, tooling like nvm will /// intercept execution and break our processes. We can work around this /// by prepending the `PATH` environment variable. -pub fn get_path_env_var(bin_dir: PathBuf) -> std::ffi::OsString { +pub fn get_path_env_var(bin_dir: &Path) -> std::ffi::OsString { let path = env::var("PATH").unwrap_or_default(); - let mut paths = vec![bin_dir]; + let mut paths = vec![bin_dir.to_path_buf()]; paths.extend(env::split_paths(&path).collect::>()); @@ -70,8 +80,9 @@ pub async fn download_file_from_url(url: &str, dest: &Path) -> Result<(), Toolch trace!( target: "moon:toolchain", - "Downloading file to {}", - color::path(dest.parent().unwrap()), + "Downloading file {} to {}", + color::url(url), + color::path(dest), ); // Ensure parent directories exist diff --git a/crates/toolchain/src/lib.rs b/crates/toolchain/src/lib.rs index 4c1e69176c6..a79e657b6ca 100644 --- a/crates/toolchain/src/lib.rs +++ b/crates/toolchain/src/lib.rs @@ -1,10 +1,11 @@ mod errors; -mod helpers; -mod tool; +pub mod helpers; +pub mod pms; mod toolchain; pub mod tools; +mod traits; pub use errors::ToolchainError; pub use helpers::get_path_env_var; -pub use tool::Tool; pub use toolchain::Toolchain; +pub use traits::{Downloadable, Executable, Installable, PackageManager, Tool}; diff --git a/crates/toolchain/src/pms/mod.rs b/crates/toolchain/src/pms/mod.rs new file mode 100644 index 00000000000..25c88d15e0b --- /dev/null +++ b/crates/toolchain/src/pms/mod.rs @@ -0,0 +1,3 @@ +pub mod npm; +pub mod pnpm; +pub mod yarn; diff --git a/crates/toolchain/src/pms/npm.rs b/crates/toolchain/src/pms/npm.rs new file mode 100644 index 00000000000..75b08ee2166 --- /dev/null +++ b/crates/toolchain/src/pms/npm.rs @@ -0,0 +1,272 @@ +use crate::errors::ToolchainError; +use crate::helpers::{get_bin_name_suffix, get_bin_version, get_path_env_var}; +use crate::tools::node::NodeTool; +use crate::traits::{Executable, Installable, Lifecycle, PackageManager}; +use crate::Toolchain; +use async_trait::async_trait; +use moon_config::NpmConfig; +use moon_logger::{color, debug, Logable}; +use moon_utils::is_ci; +use moon_utils::process::{output_to_trimmed_string, Command}; +use std::env; +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub struct NpmTool { + bin_path: PathBuf, + + pub config: NpmConfig, + + global_install_dir: Option, + + install_dir: PathBuf, +} + +impl NpmTool { + pub fn new(node: &NodeTool, config: &NpmConfig) -> Result { + let install_dir = node.get_install_dir()?.clone(); + + Ok(NpmTool { + bin_path: install_dir.join(get_bin_name_suffix("npm", "cmd", false)), + config: config.to_owned(), + global_install_dir: None, + install_dir, + }) + } + + pub fn get_global_dir(&self) -> Result<&PathBuf, ToolchainError> { + Ok(self + .global_install_dir + .as_ref() + .unwrap_or(&self.install_dir)) + } + + pub async fn install_global_dep( + &self, + package: &str, + version: &str, + ) -> Result<(), ToolchainError> { + self.create_command() + .args(["install", "-g", &format!("{}@{}", package, version)]) + .exec_capture_output() + .await?; + + Ok(()) + } + + pub async fn is_global_dep_installed(&self, package: &str) -> Result { + let output = self + .create_command() + .args(["list", "-g", package]) + .no_error_on_failure() + .exec_capture_output() + .await?; + + Ok(output.status.success()) + } +} + +impl Logable for NpmTool { + fn get_log_target(&self) -> String { + String::from("moon:toolchain:npm") + } +} + +#[async_trait] +impl Lifecycle for NpmTool { + async fn setup(&mut self, _node: &NodeTool, check_version: bool) -> Result { + if check_version { + let output = self + .create_command() + .args(["config", "get", "prefix"]) + .exec_capture_output() + .await?; + + self.global_install_dir = Some(PathBuf::from(output_to_trimmed_string(&output.stdout))); + } + + Ok(0) + } +} + +#[async_trait] +impl Installable for NpmTool { + fn get_install_dir(&self) -> Result<&PathBuf, ToolchainError> { + Ok(&self.install_dir) + } + + async fn get_installed_version(&self) -> Result { + get_bin_version(self.get_bin_path()).await + } + + async fn is_installed( + &self, + _node: &NodeTool, + check_version: bool, + ) -> Result { + let target = self.get_log_target(); + + if !self.is_executable() { + return Ok(false); + } + + if !check_version { + return Ok(true); + } + + let version = self.get_installed_version().await?; + + if self.config.version == "inherit" { + debug!( + target: &target, + "Using the version ({}) that came bundled with Node.js", version + ); + + return Ok(true); + } + + if version == self.config.version { + debug!( + target: &target, + "Package has already been installed and is on the correct version", + ); + + return Ok(true); + } + + debug!( + target: &target, + "Package is on the wrong version ({}), attempting to reinstall", version + ); + + Ok(false) + } + + async fn install(&self, node: &NodeTool) -> Result<(), ToolchainError> { + if self.config.version == "inherit" { + return Ok(()); + } + + let target = self.get_log_target(); + let package = format!("npm@{}", self.config.version); + + if node.is_corepack_aware() { + debug!( + target: &target, + "Enabling package manager with {}", + color::shell(&format!("corepack prepare {} --activate", package)) + ); + + node.exec_corepack(["prepare", &package, "--activate"]) + .await?; + } else { + debug!( + target: &target, + "Installing package manager with {}", + color::shell(&format!("npm install -g {}", package)) + ); + + self.install_global_dep("npm", &self.config.version).await?; + } + + Ok(()) + } +} + +#[async_trait] +impl Executable for NpmTool { + async fn find_bin_path(&mut self, _node: &NodeTool) -> Result<(), ToolchainError> { + // If the global has moved, be sure to reference it + let bin_path = self + .get_global_dir()? + .join(get_bin_name_suffix("npm", "cmd", false)); + + if bin_path.exists() { + self.bin_path = bin_path; + } + + Ok(()) + } + + fn get_bin_path(&self) -> &PathBuf { + &self.bin_path + } + + fn is_executable(&self) -> bool { + true + } +} + +#[async_trait] +impl PackageManager for NpmTool { + async fn dedupe_dependencies(&self, toolchain: &Toolchain) -> Result<(), ToolchainError> { + self.create_command() + .args(["dedupe"]) + .cwd(&toolchain.workspace_root) + .exec_capture_output() + .await?; + + Ok(()) + } + + async fn exec_package( + &self, + toolchain: &Toolchain, + package: &str, + args: Vec<&str>, + ) -> Result<(), ToolchainError> { + let mut exec_args = vec!["--silent", "--package", package, "--"]; + + exec_args.extend(args); + + let bin_dir = toolchain.get_node().get_install_dir()?; + let npx_path = bin_dir.join(get_bin_name_suffix("npx", "exe", false)); + + Command::new(&npx_path) + .args(exec_args) + .cwd(&toolchain.workspace_root) + .env("PATH", get_path_env_var(bin_dir)) + .exec_stream_output() + .await?; + + Ok(()) + } + + fn get_lockfile_name(&self) -> String { + String::from("package-lock.json") + } + + fn get_workspace_dependency_range(&self) -> String { + String::from("*") // Doesn't support "workspace:*" + } + + async fn install_dependencies(&self, toolchain: &Toolchain) -> Result<(), ToolchainError> { + let mut args = vec!["install"]; + + if is_ci() { + let lockfile = toolchain.workspace_root.join(self.get_lockfile_name()); + + // npm will error if using `ci` and a lockfile does not exist! + if lockfile.exists() { + args.clear(); + args.push("ci"); + } + } else { + args.push("--no-audit"); + } + + args.push("--no-fund"); + + let mut cmd = self.create_command(); + + cmd.args(args).cwd(&toolchain.workspace_root); + + if env::var("MOON_TEST_HIDE_INSTALL_OUTPUT").is_ok() { + cmd.exec_capture_output().await?; + } else { + cmd.exec_stream_output().await?; + } + + Ok(()) + } +} diff --git a/crates/toolchain/src/pms/pnpm.rs b/crates/toolchain/src/pms/pnpm.rs new file mode 100644 index 00000000000..35bad532078 --- /dev/null +++ b/crates/toolchain/src/pms/pnpm.rs @@ -0,0 +1,203 @@ +use crate::errors::ToolchainError; +use crate::helpers::{get_bin_name_suffix, get_bin_version}; +use crate::tools::node::NodeTool; +use crate::traits::{Executable, Installable, Lifecycle, PackageManager}; +use crate::Toolchain; +use async_trait::async_trait; +use moon_config::PnpmConfig; +use moon_logger::{color, debug, Logable}; +use moon_utils::is_ci; +use std::env; +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub struct PnpmTool { + bin_path: PathBuf, + + pub config: PnpmConfig, + + install_dir: PathBuf, +} + +impl PnpmTool { + pub fn new(node: &NodeTool, config: &PnpmConfig) -> Result { + let install_dir = node.get_install_dir()?.clone(); + + Ok(PnpmTool { + bin_path: install_dir.join(get_bin_name_suffix("pnpm", "cmd", false)), + config: config.to_owned(), + install_dir, + }) + } +} + +impl Logable for PnpmTool { + fn get_log_target(&self) -> String { + String::from("moon:toolchain:pnpm") + } +} + +impl Lifecycle for PnpmTool {} + +#[async_trait] +impl Installable for PnpmTool { + fn get_install_dir(&self) -> Result<&PathBuf, ToolchainError> { + Ok(&self.install_dir) + } + + async fn get_installed_version(&self) -> Result { + get_bin_version(self.get_bin_path()).await + } + + async fn is_installed( + &self, + node: &NodeTool, + check_version: bool, + ) -> Result { + let target = self.get_log_target(); + + if !self.is_executable() + || (!node.is_corepack_aware() + && !node.get_npm().is_global_dep_installed("pnpm").await?) + { + return Ok(false); + } + + if !check_version { + return Ok(true); + } + + let version = self.get_installed_version().await?; + + if version != self.config.version { + debug!( + target: &target, + "Package is on the wrong version ({}), attempting to reinstall", version + ); + + return Ok(false); + } + + debug!( + target: &target, + "Package has already been installed and is on the correct version", + ); + + Ok(true) + } + + async fn install(&self, node: &NodeTool) -> Result<(), ToolchainError> { + let target = self.get_log_target(); + let npm = node.get_npm(); + let package = format!("pnpm@{}", self.config.version); + + if node.is_corepack_aware() { + debug!( + target: &target, + "Enabling package manager with {}", + color::shell(&format!("corepack prepare {} --activate", package)) + ); + + node.exec_corepack(["prepare", &package, "--activate"]) + .await?; + } else { + debug!( + target: &target, + "Installing package manager with {}", + color::shell(&format!("npm install -g {}", package)) + ); + + npm.install_global_dep("pnpm", &self.config.version).await?; + } + + Ok(()) + } +} + +#[async_trait] +impl Executable for PnpmTool { + async fn find_bin_path(&mut self, node: &NodeTool) -> Result<(), ToolchainError> { + // If the global has moved, be sure to reference it + let bin_path = node + .get_npm() + .get_global_dir()? + .join(get_bin_name_suffix("pnpm", "cmd", false)); + + if bin_path.exists() { + self.bin_path = bin_path; + } + + Ok(()) + } + + fn get_bin_path(&self) -> &PathBuf { + &self.bin_path + } + + fn is_executable(&self) -> bool { + self.bin_path.exists() + } +} + +#[async_trait] +impl PackageManager for PnpmTool { + async fn dedupe_dependencies(&self, toolchain: &Toolchain) -> Result<(), ToolchainError> { + // pnpm doesn't support deduping, but maybe prune is good here? + // https://pnpm.io/cli/prune + self.create_command() + .arg("prune") + .cwd(&toolchain.workspace_root) + .exec_capture_output() + .await?; + + Ok(()) + } + + async fn exec_package( + &self, + toolchain: &Toolchain, + package: &str, + args: Vec<&str>, + ) -> Result<(), ToolchainError> { + // https://pnpm.io/cli/dlx + let mut exec_args = vec!["--package", package, "dlx"]; + exec_args.extend(args); + + self.create_command() + .args(exec_args) + .cwd(&toolchain.workspace_root) + .exec_stream_output() + .await?; + + Ok(()) + } + + fn get_lockfile_name(&self) -> String { + String::from("pnpm-lock.yaml") + } + + fn get_workspace_dependency_range(&self) -> String { + // https://pnpm.io/workspaces#workspace-protocol-workspace + String::from("workspace:*") + } + + async fn install_dependencies(&self, toolchain: &Toolchain) -> Result<(), ToolchainError> { + let mut args = vec!["install"]; + + if is_ci() { + args.push("--frozen-lockfile"); + } + + let mut cmd = self.create_command(); + + cmd.args(args).cwd(&toolchain.workspace_root); + + if env::var("MOON_TEST_HIDE_INSTALL_OUTPUT").is_ok() { + cmd.exec_capture_output().await?; + } else { + cmd.exec_stream_output().await?; + } + + Ok(()) + } +} diff --git a/crates/toolchain/src/pms/yarn.rs b/crates/toolchain/src/pms/yarn.rs new file mode 100644 index 00000000000..440dfcb18f5 --- /dev/null +++ b/crates/toolchain/src/pms/yarn.rs @@ -0,0 +1,279 @@ +use crate::errors::ToolchainError; +use crate::helpers::{get_bin_name_suffix, get_bin_version}; +use crate::tools::node::NodeTool; +use crate::traits::{Executable, Installable, Lifecycle, PackageManager}; +use crate::Toolchain; +use async_trait::async_trait; +use moon_config::YarnConfig; +use moon_logger::{color, debug, Logable}; +use moon_utils::is_ci; +use std::env; +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub struct YarnTool { + bin_path: PathBuf, + + pub config: YarnConfig, + + install_dir: PathBuf, +} + +impl YarnTool { + pub fn new(node: &NodeTool, config: &YarnConfig) -> Result { + let install_dir = node.get_install_dir()?.clone(); + + Ok(YarnTool { + bin_path: install_dir.join(get_bin_name_suffix("yarn", "cmd", false)), + config: config.to_owned(), + install_dir, + }) + } + + fn is_v1(&self) -> bool { + self.config.version.starts_with('1') + } +} + +impl Logable for YarnTool { + fn get_log_target(&self) -> String { + String::from("moon:toolchain:yarn") + } +} + +#[async_trait] +impl Lifecycle for YarnTool { + async fn setup(&mut self, _node: &NodeTool, check_version: bool) -> Result { + if !check_version || self.is_v1() { + return Ok(0); + } + + // We must do this here instead of `install`, because the bin path + // isn't available yet during installation, only after! + debug!( + target: &self.get_log_target(), + "Updating package manager version with {}", + color::shell(&format!("yarn set version {}", self.config.version)) + ); + + self.create_command() + .args(["set", "version", &self.config.version]) + .exec_capture_output() + .await?; + + Ok(1) + } +} + +#[async_trait] +impl Installable for YarnTool { + fn get_install_dir(&self) -> Result<&PathBuf, ToolchainError> { + Ok(&self.install_dir) + } + + async fn get_installed_version(&self) -> Result { + get_bin_version(self.get_bin_path()).await + } + + async fn is_installed( + &self, + node: &NodeTool, + check_version: bool, + ) -> Result { + let target = self.get_log_target(); + + if !self.is_executable() + || (!node.is_corepack_aware() + && !node.get_npm().is_global_dep_installed("yarn").await?) + { + return Ok(false); + } + + if !check_version { + return Ok(true); + } + + let version = self.get_installed_version().await?; + + if version != self.config.version { + debug!( + target: &target, + "Package is on the wrong version ({}), attempting to reinstall", version + ); + + return Ok(false); + } + + debug!( + target: &target, + "Package has already been installed and is on the correct version", + ); + + Ok(true) + } + + // Yarn is installed through npm, but only v1 exists in the npm registry, + // even if a consumer is using Yarn 2/3. https://www.npmjs.com/package/yarn + // Yarn >= 2 work differently than normal packages, as their runtime code + // is stored *within* the repository, and the v1 package detects it. + // Because of this, we need to always install the v1 package! + async fn install(&self, node: &NodeTool) -> Result<(), ToolchainError> { + let target = self.get_log_target(); + let npm = node.get_npm(); + let package = format!("yarn@{}", self.config.version); + + if node.is_corepack_aware() { + debug!( + target: &target, + "Enabling package manager with {}", + color::shell(&format!("corepack prepare {} --activate", package)) + ); + + node.exec_corepack(["prepare", &package, "--activate"]) + .await?; + + // v1 + } else if self.is_v1() { + debug!( + target: &target, + "Installing package with {}", + color::shell(&format!("npm install -g {}", package)) + ); + + npm.install_global_dep("yarn", &self.config.version).await?; + + // v2, v3 + } else { + debug!( + target: &target, + "Installing legacy package with {}", + color::shell("npm install -g yarn@latest") + ); + + npm.install_global_dep("yarn", "latest").await?; + } + + Ok(()) + } +} + +#[async_trait] +impl Executable for YarnTool { + async fn find_bin_path(&mut self, node: &NodeTool) -> Result<(), ToolchainError> { + // If the global has moved, be sure to reference it + let bin_path = node + .get_npm() + .get_global_dir()? + .join(get_bin_name_suffix("yarn", "cmd", false)); + + if bin_path.exists() { + self.bin_path = bin_path; + } + + Ok(()) + } + + fn get_bin_path(&self) -> &PathBuf { + &self.bin_path + } + + fn is_executable(&self) -> bool { + self.bin_path.exists() + } +} + +#[async_trait] +impl PackageManager for YarnTool { + async fn dedupe_dependencies(&self, toolchain: &Toolchain) -> Result<(), ToolchainError> { + // Yarn v1 doesnt dedupe natively, so use: + // npx yarn-deduplicate yarn.lock + if self.is_v1() { + if toolchain + .workspace_root + .join(self.get_lockfile_name()) + .exists() + { + // Will error if the lockfile does not exist! + toolchain + .get_node() + .get_npm() + .exec_package( + toolchain, + "yarn-deduplicate", + vec!["yarn-deduplicate", "yarn.lock"], + ) + .await?; + } + + // yarn dedupe + } else { + self.create_command() + .arg("dedupe") + .cwd(&toolchain.workspace_root) + .exec_capture_output() + .await?; + } + + Ok(()) + } + + async fn exec_package( + &self, + toolchain: &Toolchain, + package: &str, + args: Vec<&str>, + ) -> Result<(), ToolchainError> { + // https://yarnpkg.com/cli/dlx + let mut exec_args = vec!["dlx", "--package", package]; + exec_args.extend(args); + + self.create_command() + .args(exec_args) + .cwd(&toolchain.workspace_root) + .exec_stream_output() + .await?; + + Ok(()) + } + + fn get_lockfile_name(&self) -> String { + String::from("yarn.lock") + } + + fn get_workspace_dependency_range(&self) -> String { + if self.is_v1() { + String::from("*") + } else { + // https://yarnpkg.com/features/workspaces/#workspace-ranges-workspace + String::from("workspace:*") + } + } + + async fn install_dependencies(&self, toolchain: &Toolchain) -> Result<(), ToolchainError> { + let mut args = vec!["install"]; + + if is_ci() { + if self.is_v1() { + args.push("--check-files"); + args.push("--frozen-lockfile"); + args.push("--ignore-engines"); + args.push("--non-interactive"); + } else { + args.push("--check-cache"); + args.push("--immutable"); + } + } + + let mut cmd = self.create_command(); + + cmd.args(args).cwd(&toolchain.workspace_root); + + if env::var("MOON_TEST_HIDE_INSTALL_OUTPUT").is_ok() { + cmd.exec_capture_output().await?; + } else { + cmd.exec_stream_output().await?; + } + + Ok(()) + } +} diff --git a/crates/toolchain/src/tool.rs b/crates/toolchain/src/tool.rs deleted file mode 100644 index 6dee175d240..00000000000 --- a/crates/toolchain/src/tool.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::errors::ToolchainError; -use crate::Toolchain; -use async_trait::async_trait; -use std::path::PathBuf; -use std::process::Output; - -#[async_trait] -pub trait Tool { - /// Returns an absolute file path to the directory containing the executable binaries. - fn get_bin_dir(&self) -> PathBuf { - self.get_bin_path().parent().unwrap().to_path_buf() - } - - /// Returns an absolute file path to the executable binary for the tool. - /// This _may not exist_, as the path is composed ahead of time. - fn get_bin_path(&self) -> &PathBuf; - - /// Determine whether the tool has already been downloaded. - fn is_downloaded(&self) -> bool; - - /// Downloads the tool into the ~/.moon/temp folder, - /// and returns a file path to the downloaded binary. - async fn download(&self, host: Option<&str>) -> Result<(), ToolchainError>; - - /// Returns an absolute file path to the temporary downloaded file. - /// This _may not exist_, as the path is composed ahead of time. - /// This is typically ~/.moon/temp/. - fn get_download_path(&self) -> Option<&PathBuf>; - - /// Determine whether the tool has already been installed. - /// If `check_version` is false, avoid running the binaries as child processes - /// to extract the current version. - async fn is_installed(&self, check_version: bool) -> Result; - - /// Runs any installation steps after downloading. - /// This is typically unzipping an archive, and running any installers/binaries. - async fn install(&self, toolchain: &Toolchain) -> Result<(), ToolchainError>; - - /// Returns an absolute file path to the directory containing the downloaded tool. - /// This _may not exist_, as the path is composed ahead of time. - /// This is typically ~/.moon/tools//. - fn get_install_dir(&self) -> &PathBuf; - - /// Returns a semver version for the currently installed binary. - /// This is typically acquired by executing the binary with a --version argument. - async fn get_installed_version(&self) -> Result; -} - -#[async_trait] -pub trait PackageManager { - /// Dedupe dependencies after they have been installed. - async fn dedupe_dependencies(&self, toolchain: &Toolchain) -> Result; - - /// Download and execute a one-off package. - async fn exec_package( - &self, - toolchain: &Toolchain, - package: &str, - args: Vec<&str>, - ) -> Result; - - /// Return the name of the lockfile. - fn get_lockfile_name(&self) -> String; - - /// Return the dependency range to use when linking local workspace packages. - fn get_workspace_dependency_range(&self) -> String; - - /// Install dependencies at the root where a `package.json` exists. - async fn install_dependencies(&self, toolchain: &Toolchain) -> Result; -} diff --git a/crates/toolchain/src/toolchain.rs b/crates/toolchain/src/toolchain.rs index 53ba277416d..726c9546599 100644 --- a/crates/toolchain/src/toolchain.rs +++ b/crates/toolchain/src/toolchain.rs @@ -1,15 +1,11 @@ use crate::errors::ToolchainError; -use crate::tool::PackageManager; -use crate::tool::Tool; use crate::tools::node::NodeTool; -use crate::tools::npm::NpmTool; -use crate::tools::pnpm::PnpmTool; -use crate::tools::yarn::YarnTool; +use crate::traits::Tool; use moon_config::constants::CONFIG_DIRNAME; -use moon_config::package::PackageJson; -use moon_config::{PackageManager as PM, WorkspaceConfig}; +use moon_config::WorkspaceConfig; use moon_logger::{color, debug, trace}; use moon_utils::fs; +use moon_utils::path::get_home_dir; use std::path::{Path, PathBuf}; async fn create_dir(dir: &Path) -> Result<(), ToolchainError> { @@ -45,16 +41,13 @@ pub struct Toolchain { // Tool instances are private, as we want to lazy load them. node: Option, - npm: Option, - pnpm: Option, - yarn: Option, } impl Toolchain { pub async fn create_from_dir( - config: &WorkspaceConfig, base_dir: &Path, root_dir: &Path, + config: &WorkspaceConfig, ) -> Result { let dir = base_dir.join(CONFIG_DIRNAME); let temp_dir = dir.join("temp"); @@ -70,37 +63,15 @@ impl Toolchain { create_dir(&temp_dir).await?; create_dir(&tools_dir).await?; - // Create the instance first, so we can pass to each tool initializer let mut toolchain = Toolchain { dir, temp_dir, tools_dir, workspace_root: root_dir.to_path_buf(), node: None, - npm: None, - pnpm: None, - yarn: None, }; - // Then set the private fields with the tool instances. - // Order is IMPORTANT here, as some tools rely on others already - // being instantiated. For example, npm requires node, - // and pnpm/yarn require npm! - let node = &config.node; - - toolchain.node = Some(NodeTool::new(&toolchain, node)?); - - toolchain.npm = Some(NpmTool::new(&toolchain, &node.npm)?); - - match node.package_manager { - PM::Npm => {} - PM::Pnpm => { - toolchain.pnpm = Some(PnpmTool::new(&toolchain, node.pnpm.as_ref().unwrap())?); - } - PM::Yarn => { - toolchain.yarn = Some(YarnTool::new(&toolchain, node.yarn.as_ref().unwrap())?); - } - } + toolchain.node = Some(NodeTool::new(&toolchain, &config.node)?); Ok(toolchain) } @@ -110,173 +81,49 @@ impl Toolchain { config: &WorkspaceConfig, ) -> Result { Toolchain::create_from_dir( - config, - &fs::get_home_dir().ok_or(ToolchainError::MissingHomeDir)?, + &get_home_dir().ok_or(ToolchainError::MissingHomeDir)?, root_dir, + config, ) .await } /// Download and install all tools into the toolchain. - pub async fn setup( - &self, - root_package: &mut PackageJson, - check_versions: bool, - ) -> Result { + /// Return a count of how many tools were installed. + pub async fn setup(&mut self, check_versions: bool) -> Result { debug!( target: "moon:toolchain", "Downloading and installing tools", ); - // Install node and add engines to `package.json` - let node = self.get_node(); - let using_corepack = node.is_corepack_aware(); - let installed_node = self.load_tool(node, check_versions).await?; - - // Set the `packageManager` field on `package.json` - let mut check_manager_version = installed_node || check_versions; - let manager_version = match node.config.package_manager { - PM::Npm => format!("npm@{}", node.config.npm.version), - PM::Pnpm => format!("pnpm@{}", node.config.pnpm.as_ref().unwrap().version), - PM::Yarn => format!("yarn@{}", node.config.yarn.as_ref().unwrap().version), - }; - - if using_corepack && root_package.set_package_manager(&manager_version) { - root_package.save().await?; - check_manager_version = true; - } - - // Enable corepack before intalling package managers (when available) - if using_corepack && check_manager_version { - debug!( - target: "moon:toolchain:node", - "Enabling corepack for package manager control" - ); + let mut installed = 0; - node.exec_corepack(["enable"]).await?; + if self.node.is_some() { + let mut node = self.node.take().unwrap(); + installed += node.run_setup(self, check_versions).await?; + self.node = Some(node); } - // Install npm (should always be available even if using another package manager) - let mut installed_pm = self - .load_tool(self.get_npm(), check_manager_version) - .await?; - - // Install pnpm and yarn *after* setting the corepack package manager - if let Some(pnpm) = &self.pnpm { - installed_pm = self.load_tool(pnpm, check_manager_version).await?; - } - - if let Some(yarn) = &self.yarn { - installed_pm = self.load_tool(yarn, check_manager_version).await?; - } - - Ok(installed_node || installed_pm) + Ok(installed) } /// Uninstall all tools from the toolchain, and delete any temporary files. - pub async fn teardown(&self) -> Result<(), ToolchainError> { + pub async fn teardown(&mut self) -> Result<(), ToolchainError> { debug!( target: "moon:toolchain", "Tearing down toolchain, uninstalling tools", ); - if let Some(yarn) = &self.yarn { - self.unload_tool(yarn).await?; - } - - if let Some(pnp) = &self.pnpm { - self.unload_tool(pnp).await?; - } - - self.unload_tool(self.get_npm()).await?; - self.unload_tool(self.get_node()).await?; - - fs::remove_dir_all(&self.dir).await?; - - Ok(()) - } - - /// Load a tool into the toolchain by downloading an artifact/binary - /// into the temp folder, then installing it into the tools folder. - /// Return `true` if the tool was newly installed. - async fn load_tool( - &self, - tool: &(dyn Tool + Send + Sync), - check_version: bool, - ) -> Result { - if !tool.is_downloaded() { - tool.download(None).await?; - } - - if tool.is_installed(check_version).await? { - return Ok(false); - } else { - tool.install(self).await?; - } - - Ok(true) - } - - /// Unload the tool by removing any downloaded/installed artifacts. - /// This can be ran manually, or automatically during a failed load. - async fn unload_tool(&self, tool: &(dyn Tool + Send + Sync)) -> Result<(), ToolchainError> { - if tool.is_downloaded() { - if let Some(download_path) = tool.get_download_path() { - fs::remove_file(download_path).await?; - - trace!( - target: "moon:toolchain", "Deleted download {}", - color::path(download_path) - ); - } - } - - if tool.is_installed(false).await? { - let install_dir = tool.get_install_dir(); - - fs::remove_dir_all(install_dir).await?; - - trace!( - target: "moon:toolchain", - "Deleted installation {}", - color::path(install_dir) - ); + if self.node.is_some() { + let mut node = self.node.take().unwrap(); + node.run_teardown(self).await?; } Ok(()) } + /// Return the Node.js tool. pub fn get_node(&self) -> &NodeTool { self.node.as_ref().unwrap() } - - pub fn get_node_package_manager(&self) -> &(dyn PackageManager + Send + Sync) { - if self.pnpm.is_some() { - return self.get_pnpm().unwrap(); - } - - if self.yarn.is_some() { - return self.get_yarn().unwrap(); - } - - self.get_npm() - } - - pub fn get_npm(&self) -> &NpmTool { - self.npm.as_ref().unwrap() - } - - pub fn get_pnpm(&self) -> Option<&PnpmTool> { - match &self.pnpm { - Some(tool) => Some(tool), - None => None, - } - } - - pub fn get_yarn(&self) -> Option<&YarnTool> { - match &self.yarn { - Some(tool) => Some(tool), - None => None, - } - } } diff --git a/crates/toolchain/src/tools/mod.rs b/crates/toolchain/src/tools/mod.rs index 0c00408277a..492bc84b46f 100644 --- a/crates/toolchain/src/tools/mod.rs +++ b/crates/toolchain/src/tools/mod.rs @@ -1,4 +1 @@ pub mod node; -pub mod npm; -pub mod pnpm; -pub mod yarn; diff --git a/crates/toolchain/src/tools/node.rs b/crates/toolchain/src/tools/node.rs index f1bf789d6b7..6526bd5eab6 100644 --- a/crates/toolchain/src/tools/node.rs +++ b/crates/toolchain/src/tools/node.rs @@ -1,16 +1,20 @@ use crate::errors::ToolchainError; use crate::helpers::{ - download_file_from_url, get_bin_version, get_file_sha256_hash, get_path_env_var, unpack, + download_file_from_url, get_bin_name_suffix, get_bin_version, get_file_sha256_hash, + get_path_env_var, unpack, }; -use crate::tool::Tool; +use crate::pms::npm::NpmTool; +use crate::pms::pnpm::PnpmTool; +use crate::pms::yarn::YarnTool; +use crate::traits::{Downloadable, Executable, Installable, Lifecycle, PackageManager, Tool}; use crate::Toolchain; use async_trait::async_trait; use moon_config::constants::CONFIG_DIRNAME; use moon_config::NodeConfig; use moon_error::map_io_to_fs_error; -use moon_logger::{color, debug, error}; +use moon_logger::{color, debug, error, Logable}; use moon_utils::fs; -use moon_utils::process::{create_command, exec_command, Output}; +use moon_utils::process::Command; use semver::{Version, VersionReq}; use std::env::consts; use std::ffi::OsStr; @@ -115,64 +119,65 @@ fn verify_shasum( pub struct NodeTool { bin_path: PathBuf, - corepack_bin_path: PathBuf, + pub config: NodeConfig, download_path: PathBuf, install_dir: PathBuf, - pub config: NodeConfig, + npm: Option, + + pnpm: Option, + + yarn: Option, } impl NodeTool { pub fn new(toolchain: &Toolchain, config: &NodeConfig) -> Result { - let mut download_path = toolchain.temp_dir.clone(); - - download_path.push("node"); - download_path.push(get_download_file(&config.version)?); - - let mut install_dir = toolchain.tools_dir.clone(); + let install_dir = toolchain.tools_dir.join("node").join(&config.version); - install_dir.push("node"); - install_dir.push(&config.version); + let mut node = NodeTool { + bin_path: install_dir.join(get_bin_name_suffix("node", "exe", false)), + config: config.to_owned(), + download_path: toolchain + .temp_dir + .join("node") + .join(get_download_file(&config.version)?), + install_dir, + npm: None, + pnpm: None, + yarn: None, + }; - let mut bin_path = install_dir.clone(); - let mut corepack_bin_path = install_dir.clone(); + node.npm = Some(NpmTool::new(&node, &config.npm)?); - if consts::OS == "windows" { - bin_path.push("node.exe"); - corepack_bin_path.push("corepack.cmd"); - } else { - bin_path.push("bin/node"); - corepack_bin_path.push("bin/corepack"); + if let Some(pnpm_config) = &config.pnpm { + node.pnpm = Some(PnpmTool::new(&node, pnpm_config)?); } - debug!( - target: "moon:toolchain:node", - "Creating tool at {}", - color::path(&bin_path) - ); + if let Some(yarn_config) = &config.yarn { + node.yarn = Some(YarnTool::new(&node, yarn_config)?); + } - Ok(NodeTool { - bin_path, - corepack_bin_path, - config: config.to_owned(), - download_path, - install_dir, - }) + Ok(node) } - pub async fn exec_corepack(&self, args: I) -> Result + pub async fn exec_corepack(&self, args: I) -> Result<(), ToolchainError> where I: IntoIterator, S: AsRef, { - Ok(exec_command( - create_command(&self.corepack_bin_path) - .args(args) - .env("PATH", get_path_env_var(self.get_bin_dir())), - ) - .await?) + let corepack_path = self + .install_dir + .join(get_bin_name_suffix("corepack", "cmd", false)); + + Command::new(&corepack_path) + .args(args) + .env("PATH", get_path_env_var(corepack_path.parent().unwrap())) + .exec_capture_output() + .await?; + + Ok(()) } pub fn find_package_bin_path( @@ -180,13 +185,10 @@ impl NodeTool { package_name: &str, starting_dir: &Path, ) -> Result { - let mut bin_path = starting_dir.join("node_modules").join(".bin"); - - if consts::OS == "windows" { - bin_path.push(format!("{}.cmd", package_name)); - } else { - bin_path.push(package_name); - } + let bin_path = starting_dir + .join("node_modules") + .join(".bin") + .join(get_bin_name_suffix(package_name, "cmd", true)); if bin_path.exists() { return Ok(bin_path); @@ -195,14 +197,47 @@ impl NodeTool { // If we've reached the root of the workspace, and still haven't found // a binary, just abort with an error... if starting_dir.join(CONFIG_DIRNAME).exists() { - return Err(ToolchainError::MissingNodeModuleBin(String::from( - package_name, - ))); + return Err(ToolchainError::MissingNodeModuleBin( + package_name.to_owned(), + )); } self.find_package_bin_path(package_name, starting_dir.parent().unwrap()) } + /// Return the `npm` package manager. + pub fn get_npm(&self) -> &NpmTool { + self.npm.as_ref().unwrap() + } + + /// Return the `pnpm` package manager. + pub fn get_pnpm(&self) -> Option<&PnpmTool> { + match &self.pnpm { + Some(tool) => Some(tool), + None => None, + } + } + + /// Return the `yarn` package manager. + pub fn get_yarn(&self) -> Option<&YarnTool> { + match &self.yarn { + Some(tool) => Some(tool), + None => None, + } + } + + pub fn get_package_manager(&self) -> &(dyn PackageManager + Send + Sync) { + if self.pnpm.is_some() { + return self.get_pnpm().unwrap(); + } + + if self.yarn.is_some() { + return self.get_yarn().unwrap(); + } + + self.get_npm() + } + pub fn is_corepack_aware(&self) -> bool { let cfg_version = Version::parse(&self.config.version).unwrap(); @@ -211,46 +246,40 @@ impl NodeTool { } } -#[async_trait] -impl Tool for NodeTool { - fn is_downloaded(&self) -> bool { - let exists = self.download_path.exists(); +impl Logable for NodeTool { + fn get_log_target(&self) -> String { + String::from("moon:toolchain:node") + } +} - if exists { - debug!( - target: "moon:toolchain:node", - "Binary has already been downloaded, continuing" - ); - } else { - debug!( - target: "moon:toolchain:node", - "Binary does not exist, attempting to download" - ); - } +#[async_trait] +impl Downloadable for NodeTool { + fn get_download_path(&self) -> Result<&PathBuf, ToolchainError> { + Ok(&self.download_path) + } - exists + async fn is_downloaded(&self) -> Result { + Ok(self.get_download_path()?.exists()) } - async fn download(&self, base_host: Option<&str>) -> Result<(), ToolchainError> { + async fn download( + &self, + _toolchain: &Toolchain, + base_host: Option<&str>, + ) -> Result<(), ToolchainError> { let version = &self.config.version; let host = base_host.unwrap_or("https://nodejs.org"); + let target = self.get_log_target(); // Download the node.tar.gz archive let download_url = get_nodejs_url(version, host, &get_download_file(version)?); + let download_path = self.get_download_path()?; - download_file_from_url(&download_url, &self.download_path).await?; - - debug!( - target: "moon:toolchain:node", - "Downloading binary from {} to {}", - color::url(&download_url), - color::path(&self.download_path) - ); + download_file_from_url(&download_url, download_path).await?; // Download the SHASUMS256.txt file let shasums_url = get_nodejs_url(version, host, "SHASUMS256.txt"); - let shasums_path = self - .download_path + let shasums_path = download_path .parent() .unwrap() .join(format!("node-v{}-SHASUMS256.txt", version)); @@ -258,72 +287,115 @@ impl Tool for NodeTool { download_file_from_url(&shasums_url, &shasums_path).await?; debug!( - target: "moon:toolchain:node", + target: &target, "Verifying shasum against {}", color::url(&shasums_url), ); // Verify the binary - if let Err(error) = verify_shasum(&download_url, &self.download_path, &shasums_path) { + if let Err(error) = verify_shasum(&download_url, download_path, &shasums_path) { error!( - target: "moon:toolchain:node", + target: &target, "Shasum verification has failed. The downloaded file has been deleted, please try again." ); - fs::remove_file(&self.download_path).await?; + fs::remove_file(download_path).await?; return Err(error); } Ok(()) } +} - async fn is_installed(&self, _check_version: bool) -> Result { - if self.install_dir.exists() { - debug!( - target: "moon:toolchain:node", - "Download has already been installed and is on the correct version", - ); - - return Ok(true); - } +#[async_trait] +impl Installable for NodeTool { + fn get_install_dir(&self) -> Result<&PathBuf, ToolchainError> { + Ok(&self.install_dir) + } - debug!( - target: "moon:toolchain:node", - "Download has not been installed", - ); + async fn get_installed_version(&self) -> Result { + Ok(get_bin_version(self.get_bin_path()).await?) + } - Ok(false) + async fn is_installed( + &self, + _toolchain: &Toolchain, + _check_version: bool, + ) -> Result { + Ok(self.get_install_dir()?.exists()) } async fn install(&self, _toolchain: &Toolchain) -> Result<(), ToolchainError> { - let install_dir = self.get_install_dir(); + let download_path = self.get_download_path()?; + let install_dir = self.get_install_dir()?; let prefix = get_download_file_name(&self.config.version)?; - unpack(&self.download_path, install_dir, &prefix).await?; + unpack(download_path, install_dir, &prefix).await?; debug!( - target: "moon:toolchain:node", + target: &self.get_log_target(), "Unpacked and installed to {}", color::path(install_dir) ); Ok(()) } +} + +#[async_trait] +impl Executable for NodeTool { + async fn find_bin_path(&mut self, _toolchain: &Toolchain) -> Result<(), ToolchainError> { + Ok(()) + } fn get_bin_path(&self) -> &PathBuf { &self.bin_path } - fn get_download_path(&self) -> Option<&PathBuf> { - Some(&self.download_path) + fn is_executable(&self) -> bool { + true } +} - fn get_install_dir(&self) -> &PathBuf { - &self.install_dir - } +#[async_trait] +impl Lifecycle for NodeTool { + async fn setup( + &mut self, + _toolchain: &Toolchain, + check_version: bool, + ) -> Result { + if self.is_corepack_aware() && check_version { + debug!( + target: &self.get_log_target(), + "Enabling corepack for package manager control" + ); - async fn get_installed_version(&self) -> Result { - Ok(get_bin_version(self.get_bin_path()).await?) + self.exec_corepack(["enable"]).await?; + } + + let mut installed = 0; + + if self.npm.is_some() { + let mut npm = self.npm.take().unwrap(); + installed += npm.run_setup(self, check_version).await?; + self.npm = Some(npm); + } + + if self.pnpm.is_some() { + let mut pnpm = self.pnpm.take().unwrap(); + installed += pnpm.run_setup(self, check_version).await?; + self.pnpm = Some(pnpm); + } + + if self.yarn.is_some() { + let mut yarn = self.yarn.take().unwrap(); + installed += yarn.run_setup(self, check_version).await?; + self.yarn = Some(yarn); + } + + Ok(installed) } } + +impl Tool for NodeTool {} diff --git a/crates/toolchain/src/tools/npm.rs b/crates/toolchain/src/tools/npm.rs deleted file mode 100644 index 16db378e9ce..00000000000 --- a/crates/toolchain/src/tools/npm.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::errors::ToolchainError; -use crate::helpers::{get_bin_version, get_path_env_var}; -use crate::tool::{PackageManager, Tool}; -use crate::Toolchain; -use async_trait::async_trait; -use moon_config::NpmConfig; -use moon_logger::{color, debug, trace}; -use moon_utils::is_ci; -use moon_utils::process::{create_command, exec_command, Output}; -use std::env::consts; -use std::path::PathBuf; - -#[derive(Clone, Debug)] -pub struct NpmTool { - bin_path: PathBuf, - - install_dir: PathBuf, - - npx_path: PathBuf, - - pub config: NpmConfig, -} - -impl NpmTool { - pub fn new(toolchain: &Toolchain, config: &NpmConfig) -> Result { - let install_dir = toolchain.get_node().get_install_dir().clone(); - let mut bin_path = install_dir.clone(); - let mut npx_path = install_dir.clone(); - - if consts::OS == "windows" { - bin_path.push("npm.cmd"); - npx_path.push("npx.cmd"); - } else { - bin_path.push("bin/npm"); - npx_path.push("bin/npx"); - } - - debug!( - target: "moon:toolchain:npm", - "Creating tool at {}", - color::path(&bin_path) - ); - - Ok(NpmTool { - bin_path, - config: config.to_owned(), - install_dir, - npx_path, - }) - } - - pub async fn add_global_dep(&self, name: &str, version: &str) -> Result<(), ToolchainError> { - let package = format!("{}@{}", name, version); - - exec_command( - create_command(self.get_bin_path()) - .args(["install", "-g", &package]) - .env("PATH", get_path_env_var(self.get_bin_dir())) - .current_dir(&self.install_dir), - ) - .await?; - - Ok(()) - } -} - -#[async_trait] -impl Tool for NpmTool { - fn is_downloaded(&self) -> bool { - true - } - - async fn download(&self, _host: Option<&str>) -> Result<(), ToolchainError> { - trace!( - target: "moon:toolchain:npm", - "No download required as it comes bundled with Node.js" - ); - - Ok(()) // This is handled by node - } - - async fn is_installed(&self, check_version: bool) -> Result { - if self.bin_path.exists() { - if !check_version { - return Ok(true); - } - - let version = self.get_installed_version().await?; - - if self.config.version == "inherit" { - debug!( - target: "moon:toolchain:npm", - "Using the version ({}) that came bundled with Node.js", - version - ); - - return Ok(true); - } - - if version == self.config.version { - debug!( - target: "moon:toolchain:npm", - "Package has already been installed and is on the correct version", - ); - - return Ok(true); - } - - debug!( - target: "moon:toolchain:npm", - "Package is on the wrong version ({}), attempting to reinstall", - version - ); - } - - Ok(false) - } - - async fn install(&self, toolchain: &Toolchain) -> Result<(), ToolchainError> { - if self.config.version == "inherit" { - return Ok(()); - } - - let package = format!("npm@{}", self.config.version); - - if toolchain.get_node().is_corepack_aware() { - debug!( - target: "moon:toolchain:npm", - "Enabling package manager with {}", - color::shell(&format!("corepack prepare {} --activate", package)) - ); - - toolchain - .get_node() - .exec_corepack(["prepare", &package, "--activate"]) - .await?; - } else { - debug!( - target: "moon:toolchain:npm", - "Installing package manager with {}", - color::shell(&format!("npm install -g {}", package)) - ); - - self.add_global_dep("npm", self.config.version.as_str()) - .await?; - } - - Ok(()) - } - - fn get_bin_path(&self) -> &PathBuf { - &self.bin_path - } - - fn get_download_path(&self) -> Option<&PathBuf> { - None - } - - fn get_install_dir(&self) -> &PathBuf { - &self.install_dir - } - - async fn get_installed_version(&self) -> Result { - Ok(get_bin_version(self.get_bin_path()).await?) - } -} - -#[async_trait] -impl PackageManager for NpmTool { - async fn dedupe_dependencies(&self, toolchain: &Toolchain) -> Result { - Ok(exec_command( - create_command(self.get_bin_path()) - .args(["dedupe"]) - .current_dir(&toolchain.workspace_root) - .env("PATH", get_path_env_var(self.get_bin_dir())), - ) - .await?) - } - - async fn exec_package( - &self, - toolchain: &Toolchain, - package: &str, - args: Vec<&str>, - ) -> Result { - let mut exec_args = vec!["--package", package, "--"]; - - exec_args.extend(args); - - Ok(exec_command( - create_command(&self.npx_path) - .args(exec_args) - .current_dir(&toolchain.workspace_root) - .env("PATH", get_path_env_var(self.get_bin_dir())), - ) - .await?) - } - - fn get_lockfile_name(&self) -> String { - String::from("package-lock.json") - } - - fn get_workspace_dependency_range(&self) -> String { - // Doesn't support "workspace:*" - String::from("*") - } - - async fn install_dependencies(&self, toolchain: &Toolchain) -> Result { - Ok(exec_command( - create_command(self.get_bin_path()) - .args([if is_ci() { "ci" } else { "install" }]) - .current_dir(&toolchain.workspace_root) - .env("PATH", get_path_env_var(self.get_bin_dir())), - ) - .await?) - } -} diff --git a/crates/toolchain/src/tools/pnpm.rs b/crates/toolchain/src/tools/pnpm.rs deleted file mode 100644 index d849570391b..00000000000 --- a/crates/toolchain/src/tools/pnpm.rs +++ /dev/null @@ -1,194 +0,0 @@ -use crate::errors::ToolchainError; -use crate::helpers::{get_bin_version, get_path_env_var}; -use crate::tool::{PackageManager, Tool}; -use crate::Toolchain; -use async_trait::async_trait; -use moon_config::PnpmConfig; -use moon_logger::{color, debug, trace}; -use moon_utils::is_ci; -use moon_utils::process::{create_command, exec_command, Output}; -use std::env::consts; -use std::path::PathBuf; - -#[derive(Clone, Debug)] -pub struct PnpmTool { - bin_path: PathBuf, - - install_dir: PathBuf, - - pub config: PnpmConfig, -} - -impl PnpmTool { - pub fn new(toolchain: &Toolchain, config: &PnpmConfig) -> Result { - let install_dir = toolchain.get_node().get_install_dir().clone(); - let mut bin_path = install_dir.clone(); - - if consts::OS == "windows" { - bin_path.push("pnpm.cmd"); - } else { - bin_path.push("bin/pnpm"); - } - - debug!( - target: "moon:toolchain:pnpm", - "Creating tool at {}", - color::path(&bin_path) - ); - - Ok(PnpmTool { - bin_path, - config: config.to_owned(), - install_dir, - }) - } -} - -#[async_trait] -impl Tool for PnpmTool { - fn is_downloaded(&self) -> bool { - true - } - - async fn download(&self, _host: Option<&str>) -> Result<(), ToolchainError> { - trace!( - target: "moon:toolchain:pnpm", - "No download required as it comes bundled with Node.js" - ); - - Ok(()) // This is handled by node - } - - async fn is_installed(&self, check_version: bool) -> Result { - if self.bin_path.exists() { - if !check_version { - return Ok(true); - } - - let version = self.get_installed_version().await?; - - if version == self.config.version { - debug!( - target: "moon:toolchain:pnpm", - "Package has already been installed and is on the correct version", - ); - - return Ok(true); - } - - debug!( - target: "moon:toolchain:pnpm", - "Package is on the wrong version ({}), attempting to reinstall", - version - ); - } - - Ok(false) - } - - async fn install(&self, toolchain: &Toolchain) -> Result<(), ToolchainError> { - let package = format!("pnpm@{}", self.config.version); - - if toolchain.get_node().is_corepack_aware() { - debug!( - target: "moon:toolchain:pnpm", - "Enabling package manager with {}", - color::shell(&format!("corepack prepare {} --activate", package)) - ); - - toolchain - .get_node() - .exec_corepack(["prepare", &package, "--activate"]) - .await?; - } else { - debug!( - target: "moon:toolchain:pnpm", - "Installing package manager with {}", - color::shell(&format!("npm install -g {}", package)) - ); - - toolchain - .get_npm() - .add_global_dep("pnpm", self.config.version.as_str()) - .await?; - } - - Ok(()) - } - - fn get_bin_path(&self) -> &PathBuf { - &self.bin_path - } - - fn get_download_path(&self) -> Option<&PathBuf> { - None - } - - fn get_install_dir(&self) -> &PathBuf { - &self.install_dir - } - - async fn get_installed_version(&self) -> Result { - Ok(get_bin_version(self.get_bin_path()).await?) - } -} - -#[async_trait] -impl PackageManager for PnpmTool { - async fn dedupe_dependencies(&self, toolchain: &Toolchain) -> Result { - // pnpm doesn't support deduping, but maybe prune is good here? - // https://pnpm.io/cli/prune - Ok(exec_command( - create_command(self.get_bin_path()) - .args(["prune"]) - .current_dir(&toolchain.workspace_root) - .env("PATH", get_path_env_var(self.get_bin_dir())), - ) - .await?) - } - - async fn exec_package( - &self, - toolchain: &Toolchain, - package: &str, - args: Vec<&str>, - ) -> Result { - let mut exec_args = vec!["--package", package, "dlx"]; - - exec_args.extend(args); - - // https://pnpm.io/cli/dlx - Ok(exec_command( - create_command(self.get_bin_path()) - .args(exec_args) - .current_dir(&toolchain.workspace_root) - .env("PATH", get_path_env_var(self.get_bin_dir())), - ) - .await?) - } - - fn get_lockfile_name(&self) -> String { - String::from("pnpm-lock.yaml") - } - - fn get_workspace_dependency_range(&self) -> String { - // https://pnpm.io/workspaces#workspace-protocol-workspace - String::from("workspace:*") - } - - async fn install_dependencies(&self, toolchain: &Toolchain) -> Result { - let mut args = vec!["install"]; - - if is_ci() { - args.push("--frozen-lockfile"); - } - - Ok(exec_command( - create_command(self.get_bin_path()) - .args(args) - .current_dir(&toolchain.workspace_root) - .env("PATH", get_path_env_var(self.get_bin_dir())), - ) - .await?) - } -} diff --git a/crates/toolchain/src/tools/yarn.rs b/crates/toolchain/src/tools/yarn.rs deleted file mode 100644 index 647c5445b44..00000000000 --- a/crates/toolchain/src/tools/yarn.rs +++ /dev/null @@ -1,266 +0,0 @@ -use crate::errors::ToolchainError; -use crate::helpers::{get_bin_version, get_path_env_var}; -use crate::tool::{PackageManager, Tool}; -use crate::Toolchain; -use async_trait::async_trait; -use moon_config::YarnConfig; -use moon_logger::{color, debug, trace}; -use moon_utils::is_ci; -use moon_utils::process::{create_command, exec_command, Output}; -use std::env::consts; -use std::path::PathBuf; - -#[derive(Clone, Debug)] -pub struct YarnTool { - bin_path: PathBuf, - - install_dir: PathBuf, - - pub config: YarnConfig, -} - -impl YarnTool { - pub fn new(toolchain: &Toolchain, config: &YarnConfig) -> Result { - let install_dir = toolchain.get_node().get_install_dir().clone(); - let mut bin_path = install_dir.clone(); - - if consts::OS == "windows" { - bin_path.push("yarn.cmd"); - } else { - bin_path.push("bin/yarn"); - } - - debug!( - target: "moon:toolchain:yarn", - "Creating tool at {}", - color::path(&bin_path) - ); - - Ok(YarnTool { - bin_path, - config: config.to_owned(), - install_dir, - }) - } - - fn is_v1(&self) -> bool { - self.config.version.starts_with('1') - } -} - -#[async_trait] -impl Tool for YarnTool { - fn is_downloaded(&self) -> bool { - true - } - - async fn download(&self, _host: Option<&str>) -> Result<(), ToolchainError> { - trace!( - target: "moon:toolchain:yarn", - "No download required as it comes bundled with Node.js" - ); - - Ok(()) // This is handled by node - } - - async fn is_installed(&self, check_version: bool) -> Result { - if self.bin_path.exists() { - if !check_version { - return Ok(true); - } - - let version = self.get_installed_version().await?; - - if version == self.config.version { - debug!( - target: "moon:toolchain:yarn", - "Package has already been installed and is on the correct version", - ); - - return Ok(true); - } - - debug!( - target: "moon:toolchain:yarn", - "Package is on the wrong version ({}), attempting to reinstall", - version - ); - } - - Ok(false) - } - - // Yarn is installed through npm, but only v1 exists in the npm registry, - // even if a consumer is using Yarn 2/3. https://www.npmjs.com/package/yarn - // Yarn >= 2 work differently than normal packages, as their runtime code - // is stored *within* the repository, and the v1 package detects it. - // Because of this, we need to always install the v1 package! - async fn install(&self, toolchain: &Toolchain) -> Result<(), ToolchainError> { - let node = toolchain.get_node(); - let npm = toolchain.get_npm(); - - if self.is_v1() { - let package = format!("yarn@{}", self.config.version); - - if node.is_corepack_aware() { - debug!( - target: "moon:toolchain:yarn", - "Enabling package manager with {}", - color::shell(&format!("corepack prepare {} --activate", package)) - ); - - node.exec_corepack(["prepare", &package, "--activate"]) - .await?; - } else { - debug!( - target: "moon:toolchain:yarn", - "Installing package with {}", - color::shell(&format!("npm install -g {}", package)) - ); - - npm.add_global_dep("yarn", &self.config.version).await?; - } - } else { - if node.is_corepack_aware() { - debug!( - target: "moon:toolchain:yarn", - "Enabling package manager with {}", - color::shell("corepack prepare yarn --activate") - ); - - node.exec_corepack(["prepare", "yarn", "--activate"]) - .await?; - } else { - debug!( - target: "moon:toolchain:yarn", - "Installing legacy package with {}", - color::shell("npm install -g yarn@latest") - ); - - npm.add_global_dep("yarn", "latest").await?; - } - - debug!( - target: "moon:toolchain:yarn", - "Installing package manager with {}", - color::shell(&format!("yarn set version {}", self.config.version)) - ); - - exec_command( - create_command(self.get_bin_path()) - .args(["set", "version", &self.config.version]) - .current_dir(&toolchain.workspace_root) - .env("PATH", get_path_env_var(self.get_bin_dir())), - ) - .await?; - } - - Ok(()) - } - - fn get_bin_path(&self) -> &PathBuf { - &self.bin_path - } - - fn get_download_path(&self) -> Option<&PathBuf> { - None - } - - fn get_install_dir(&self) -> &PathBuf { - &self.install_dir - } - - async fn get_installed_version(&self) -> Result { - Ok(get_bin_version(self.get_bin_path()).await?) - } -} - -#[async_trait] -impl PackageManager for YarnTool { - async fn dedupe_dependencies(&self, toolchain: &Toolchain) -> Result { - // Yarn v1 doesnt dedupe natively, so use: - // npx yarn-deduplicate yarn.lock - if self.is_v1() { - Ok(toolchain - .get_npm() - .exec_package( - toolchain, - "yarn-deduplicate", - vec!["yarn-deduplicate", "yarn.lock"], - ) - .await?) - - // yarn dedupe - } else { - Ok(exec_command( - create_command(self.get_bin_path()) - .args(["dedupe"]) - .current_dir(&toolchain.workspace_root) - .env("PATH", get_path_env_var(self.get_bin_dir())), - ) - .await?) - } - } - - async fn exec_package( - &self, - toolchain: &Toolchain, - package: &str, - args: Vec<&str>, - ) -> Result { - let mut exec_args = vec!["dlx", "--package", package]; - - exec_args.extend(args); - - // https://yarnpkg.com/cli/dlx - Ok(exec_command( - create_command(self.get_bin_path()) - .args(exec_args) - .current_dir(&toolchain.workspace_root) - .env("PATH", get_path_env_var(self.get_bin_dir())), - ) - .await?) - } - - fn get_lockfile_name(&self) -> String { - String::from("yarn.lock") - } - - fn get_workspace_dependency_range(&self) -> String { - if self.is_v1() { - String::from("*") - } else { - // https://yarnpkg.com/features/workspaces/#workspace-ranges-workspace - String::from("workspace:*") - } - } - - async fn install_dependencies(&self, toolchain: &Toolchain) -> Result { - let mut args = vec!["install"]; - - if is_ci() { - if self.is_v1() { - args.push("--frozen-lockfile"); - args.push("--non-interactive"); - - if is_ci() { - args.push("--check-files"); - } - } else { - args.push("--immutable"); - - if is_ci() { - args.push("--check-cache"); - } - } - } - - Ok(exec_command( - create_command(self.get_bin_path()) - .args(args) - .current_dir(&toolchain.workspace_root) - .env("PATH", get_path_env_var(self.get_bin_dir())), - ) - .await?) - } -} diff --git a/crates/toolchain/src/traits.rs b/crates/toolchain/src/traits.rs new file mode 100644 index 00000000000..ba317c0a5eb --- /dev/null +++ b/crates/toolchain/src/traits.rs @@ -0,0 +1,266 @@ +use crate::errors::ToolchainError; +use crate::helpers::get_path_env_var; +use crate::Toolchain; +use async_trait::async_trait; +use moon_logger::{debug, Logable}; +use moon_utils::process::Command; +use moon_utils::{fs, is_offline}; +use std::path::PathBuf; + +#[async_trait] +pub trait Downloadable: Send + Sync + Logable { + /// Returns an absolute file path to the downloaded file. + /// This _may not exist_, as the path is composed ahead of time. + /// This is typically ~/.moon/temp/. + fn get_download_path(&self) -> Result<&PathBuf, ToolchainError>; + + /// Determine whether the tool has already been downloaded. + async fn is_downloaded(&self) -> Result; + + /// Downloads the tool into the ~/.moon/temp folder. + async fn download( + &self, + parent: &T, + host: Option<&str>, // Host to download from + ) -> Result<(), ToolchainError>; + + /// Delete the downloaded file(s). + async fn undownload(&self, _parent: &T) -> Result<(), ToolchainError> { + fs::remove_file(self.get_download_path()?).await?; + + Ok(()) + } + + /// Run the download process: check if downloaded -> download or skip. + async fn run_download(&self, parent: &T) -> Result<(), ToolchainError> { + let target = self.get_log_target(); + + if self.is_downloaded().await? { + debug!( + target: &target, + "Tool has already been downloaded, continuing" + ); + } else { + debug!(target: &target, "Tool has not been downloaded, attempting"); + + if is_offline() { + return Err(ToolchainError::InternetConnectionRequired); + } + + self.download(parent, None).await?; + } + + Ok(()) + } + + /// Run the undownload process: check if downloaded -> delete files. + async fn run_undownload(&self, parent: &T) -> Result<(), ToolchainError> { + if self.is_downloaded().await? { + self.undownload(parent).await?; + + debug!(target: &self.get_log_target(), "Deleted download files"); + } + + Ok(()) + } +} + +#[async_trait] +pub trait Installable: Send + Sync + Logable { + /// Returns an absolute file path to the directory containing the installed tool. + /// This is typically ~/.moon/tools//. + fn get_install_dir(&self) -> Result<&PathBuf, ToolchainError>; + + /// Returns a semver version for the currently installed binary. + /// This is typically acquired by executing the binary with a `--version` argument. + async fn get_installed_version(&self) -> Result; + + /// Determine whether the tool has already been installed. + /// If `check_version` is false, avoid running the binaries as child processes + /// to extract the current version. + async fn is_installed(&self, parent: &T, check_version: bool) -> Result; + + /// Runs any installation steps after downloading. + /// This is typically unzipping an archive, and running any installers/binaries. + async fn install(&self, parent: &T) -> Result<(), ToolchainError>; + + /// Delete the installation. + async fn uninstall(&self, _parent: &T) -> Result<(), ToolchainError> { + fs::remove_dir_all(self.get_install_dir()?).await?; + + Ok(()) + } + + /// Run the install process: check if installed & on the correct version -> + /// install or skip. Return `true` if the tool was installed. + async fn run_install(&self, parent: &T, check_version: bool) -> Result { + let target = self.get_log_target(); + + if self.is_installed(parent, check_version).await? { + debug!( + target: &target, + "Tool has already been installed, continuing" + ); + } else { + debug!(target: &target, "Tool has not been installed, attempting"); + + if is_offline() { + return Err(ToolchainError::InternetConnectionRequired); + } + + self.install(parent).await?; + + return Ok(true); + } + + Ok(false) + } + + /// Run the uninstall process: check if installed -> uninstall. + async fn run_uninstall(&self, parent: &T) -> Result<(), ToolchainError> { + if self.is_installed(parent, false).await? { + self.uninstall(parent).await?; + + debug!(target: &self.get_log_target(), "Uninstalled tool"); + } + + Ok(()) + } +} + +#[async_trait] +pub trait Executable: Send + Sync { + /// Find the absolute file path to the binary that will be executed. + /// This happens after a tool has been downloaded/installed. + async fn find_bin_path(&mut self, parent: &T) -> Result<(), ToolchainError>; + + /// Returns an absolute file path to the executable binary for the tool. + fn get_bin_path(&self) -> &PathBuf; + + /// Return true if the binary exists and is executable. + fn is_executable(&self) -> bool; +} + +#[async_trait] +pub trait Lifecycle: Send + Sync { + /// Setup the tool once it has been downloaded and installed. + /// Return a count of how many sub-tools were installed. + async fn setup(&mut self, _parent: &T, _check_version: bool) -> Result { + Ok(0) + } + + /// Teardown the tool once it has been uninstalled. + async fn teardown(&mut self, _parent: &T) -> Result<(), ToolchainError> { + Ok(()) + } +} + +#[async_trait] +pub trait Tool: + Send + + Sync + + Logable + + Downloadable + + Installable + + Executable + + Lifecycle +{ + /// Download and install the tool within the toolchain. + /// Once complete, trigger the setup hook, and return a count + /// of how many sub-tools were installed. + async fn run_setup( + &mut self, + toolchain: &Toolchain, + check_version: bool, + ) -> Result { + let mut installed = 0; + + self.run_download(toolchain).await?; + + if self.run_install(toolchain, check_version).await? { + installed += 1; + } + + self.find_bin_path(toolchain).await?; + + installed += self + .setup(toolchain, installed > 0 || check_version) + .await?; + + Ok(installed) + } + + /// Teardown the tool by removing any downloaded/installed artifacts. + /// This can be ran manually, or automatically during a failed load. + async fn run_teardown(&mut self, toolchain: &Toolchain) -> Result<(), ToolchainError> { + self.run_undownload(toolchain).await?; + self.run_uninstall(toolchain).await?; + self.teardown(toolchain).await?; + + Ok(()) + } +} + +#[async_trait] +pub trait PackageManager: + Send + Sync + Logable + Installable + Executable + Lifecycle +{ + /// Create a command to run that wraps the binary. + fn create_command(&self) -> Command { + let bin_path = self.get_bin_path(); + + let mut cmd = Command::new(bin_path); + cmd.env("PATH", get_path_env_var(bin_path.parent().unwrap())); + cmd + } + + /// Dedupe dependencies after they have been installed. + async fn dedupe_dependencies(&self, toolchain: &Toolchain) -> Result<(), ToolchainError>; + + /// Download and execute a one-off package. + async fn exec_package( + &self, + toolchain: &Toolchain, + package: &str, + args: Vec<&str>, + ) -> Result<(), ToolchainError>; + + /// Return the name of the lockfile. + fn get_lockfile_name(&self) -> String; + + /// Return the name of the manifest. + fn get_manifest_name(&self) -> String { + String::from("package.json") + } + + /// Return the dependency range to use when linking local workspace packages. + fn get_workspace_dependency_range(&self) -> String; + + /// Install dependencies for a defined manifest. + async fn install_dependencies(&self, toolchain: &Toolchain) -> Result<(), ToolchainError>; + + /// Install the package manager within the tool. Once complete, + /// trigger the setup hook, and return a count + /// of how many sub-tools were installed. + async fn run_setup(&mut self, parent: &T, check_version: bool) -> Result { + let mut installed = 0; + + if self.run_install(parent, check_version).await? { + installed += 1; + } + + self.find_bin_path(parent).await?; + + installed += self.setup(parent, check_version).await?; + + Ok(installed) + } + + /// Uninstall the package manager from the parent tool. + async fn run_teardown(&mut self, parent: &T) -> Result<(), ToolchainError> { + self.run_uninstall(parent).await?; + self.teardown(parent).await?; + + Ok(()) + } +} diff --git a/crates/toolchain/tests/node_test.rs b/crates/toolchain/tests/node_test.rs index 62dd4e15c15..d97767a5041 100644 --- a/crates/toolchain/tests/node_test.rs +++ b/crates/toolchain/tests/node_test.rs @@ -1,28 +1,28 @@ use moon_config::WorkspaceConfig; -use moon_toolchain::tools::node::NodeTool; -use moon_toolchain::{Tool, Toolchain}; +use moon_toolchain::helpers::get_bin_name_suffix; +use moon_toolchain::{Downloadable, Executable, Installable, Toolchain}; use predicates::prelude::*; use std::env; use std::path::PathBuf; -async fn create_node_tool() -> (NodeTool, assert_fs::TempDir) { +async fn create_node_tool() -> (Toolchain, assert_fs::TempDir) { let base_dir = assert_fs::TempDir::new().unwrap(); let mut config = WorkspaceConfig::default(); config.node.version = String::from("1.0.0"); - let toolchain = Toolchain::create_from_dir(&config, base_dir.path(), &env::temp_dir()) + let toolchain = Toolchain::create_from_dir(base_dir.path(), &env::temp_dir(), &config) .await .unwrap(); - (toolchain.get_node().to_owned(), base_dir) + (toolchain, base_dir) } fn get_download_file() -> &'static str { - if env::consts::OS == "windows" { + if cfg!(windows) { "node-v1.0.0-win-x64.zip" - } else if env::consts::OS == "macos" { + } else if cfg!(target_os = "macos") { "node-v1.0.0-darwin-x64.tar.gz" } else { "node-v1.0.0-linux-x64.tar.gz" @@ -31,7 +31,8 @@ fn get_download_file() -> &'static str { #[tokio::test] async fn generates_paths() { - let (node, temp_dir) = create_node_tool().await; + let (toolchain, temp_dir) = create_node_tool().await; + let node = toolchain.get_node(); // We have to use join a lot to test on windows assert!(predicates::str::ends_with( @@ -42,18 +43,13 @@ async fn generates_paths() { .to_str() .unwrap() ) - .eval(node.get_install_dir().to_str().unwrap())); + .eval(node.get_install_dir().unwrap().to_str().unwrap())); - let mut bin_path = PathBuf::from(".moon") + let bin_path = PathBuf::from(".moon") .join("tools") .join("node") - .join("1.0.0"); - - if env::consts::OS == "windows" { - bin_path = bin_path.join("node.exe"); - } else { - bin_path = bin_path.join("bin").join("node"); - } + .join("1.0.0") + .join(get_bin_name_suffix("node", "exe", false)); assert!(predicates::str::ends_with(bin_path.to_str().unwrap()) .eval(node.get_bin_path().to_str().unwrap())); @@ -77,16 +73,17 @@ mod download { #[tokio::test] async fn is_downloaded_checks() { - let (node, temp_dir) = create_node_tool().await; + let (toolchain, temp_dir) = create_node_tool().await; + let node = toolchain.get_node(); - assert!(!node.is_downloaded()); + assert!(!node.is_downloaded().await.unwrap()); let dl_path = node.get_download_path().unwrap(); std::fs::create_dir_all(dl_path.parent().unwrap()).unwrap(); std::fs::write(dl_path, "").unwrap(); - assert!(node.is_downloaded()); + assert!(node.is_downloaded().await.unwrap()); std::fs::remove_file(dl_path).unwrap(); @@ -95,7 +92,8 @@ mod download { #[tokio::test] async fn downloads_to_temp_dir() { - let (node, temp_dir) = create_node_tool().await; + let (toolchain, temp_dir) = create_node_tool().await; + let node = toolchain.get_node(); assert!(!node.get_download_path().unwrap().exists()); @@ -110,7 +108,9 @@ mod download { .with_body("9a3a45d01531a20e89ac6ae10b0b0beb0492acd7216a368aa062d1a5fecaf9cd node-v1.0.0-darwin-x64.tar.gz\n9a3a45d01531a20e89ac6ae10b0b0beb0492acd7216a368aa062d1a5fecaf9cd node-v1.0.0-linux-x64.tar.gz\n9a3a45d01531a20e89ac6ae10b0b0beb0492acd7216a368aa062d1a5fecaf9cd node-v1.0.0-win-x64.zip\n") .create(); - node.download(Some(&mockito::server_url())).await.unwrap(); + node.download(&toolchain, Some(&mockito::server_url())) + .await + .unwrap(); archive.assert(); shasums.assert(); @@ -123,7 +123,8 @@ mod download { #[tokio::test] #[should_panic(expected = "InvalidShasum")] async fn fails_on_invalid_shasum() { - let (node, temp_dir) = create_node_tool().await; + let (toolchain, temp_dir) = create_node_tool().await; + let node = toolchain.get_node(); let archive = mock( "GET", @@ -138,7 +139,9 @@ mod download { ) .create(); - node.download(Some(&mockito::server_url())).await.unwrap(); + node.download(&toolchain, Some(&mockito::server_url())) + .await + .unwrap(); archive.assert(); shasums.assert(); diff --git a/crates/toolchain/tests/npm_test.rs b/crates/toolchain/tests/npm_test.rs index c4589a15af5..0173eac3986 100644 --- a/crates/toolchain/tests/npm_test.rs +++ b/crates/toolchain/tests/npm_test.rs @@ -1,11 +1,11 @@ use moon_config::WorkspaceConfig; -use moon_toolchain::tools::npm::NpmTool; -use moon_toolchain::{Tool, Toolchain}; +use moon_toolchain::helpers::get_bin_name_suffix; +use moon_toolchain::{Executable, Installable, Toolchain}; use predicates::prelude::*; use std::env; use std::path::PathBuf; -async fn create_npm_tool() -> (NpmTool, assert_fs::TempDir) { +async fn create_npm_tool() -> (Toolchain, assert_fs::TempDir) { let base_dir = assert_fs::TempDir::new().unwrap(); let mut config = WorkspaceConfig::default(); @@ -13,16 +13,17 @@ async fn create_npm_tool() -> (NpmTool, assert_fs::TempDir) { config.node.version = String::from("1.0.0"); config.node.npm.version = String::from("6.0.0"); - let toolchain = Toolchain::create_from_dir(&config, base_dir.path(), &env::temp_dir()) + let toolchain = Toolchain::create_from_dir(base_dir.path(), &env::temp_dir(), &config) .await .unwrap(); - (toolchain.get_npm().to_owned(), base_dir) + (toolchain, base_dir) } #[tokio::test] async fn generates_paths() { - let (npm, temp_dir) = create_npm_tool().await; + let (toolchain, temp_dir) = create_npm_tool().await; + let npm = toolchain.get_node().get_npm(); assert!(predicates::str::ends_with( PathBuf::from(".moon") @@ -32,18 +33,13 @@ async fn generates_paths() { .to_str() .unwrap() ) - .eval(npm.get_install_dir().to_str().unwrap())); + .eval(npm.get_install_dir().unwrap().to_str().unwrap())); - let mut bin_path = PathBuf::from(".moon") + let bin_path = PathBuf::from(".moon") .join("tools") .join("node") - .join("1.0.0"); - - if env::consts::OS == "windows" { - bin_path = bin_path.join("npm.cmd"); - } else { - bin_path = bin_path.join("bin").join("npm"); - } + .join("1.0.0") + .join(get_bin_name_suffix("npm", "cmd", false)); assert!(predicates::str::ends_with(bin_path.to_str().unwrap()) .eval(npm.get_bin_path().to_str().unwrap())); diff --git a/crates/toolchain/tests/pnpm_test.rs b/crates/toolchain/tests/pnpm_test.rs index e0c35c15bbb..037a418b7fd 100644 --- a/crates/toolchain/tests/pnpm_test.rs +++ b/crates/toolchain/tests/pnpm_test.rs @@ -1,11 +1,11 @@ use moon_config::{PackageManager, PnpmConfig, WorkspaceConfig}; -use moon_toolchain::tools::pnpm::PnpmTool; -use moon_toolchain::{Tool, Toolchain}; +use moon_toolchain::helpers::get_bin_name_suffix; +use moon_toolchain::{Executable, Installable, Toolchain}; use predicates::prelude::*; use std::env; use std::path::PathBuf; -async fn create_pnpm_tool() -> (PnpmTool, assert_fs::TempDir) { +async fn create_pnpm_tool() -> (Toolchain, assert_fs::TempDir) { let base_dir = assert_fs::TempDir::new().unwrap(); let mut config = WorkspaceConfig::default(); @@ -16,16 +16,17 @@ async fn create_pnpm_tool() -> (PnpmTool, assert_fs::TempDir) { version: String::from("6.0.0"), }); - let toolchain = Toolchain::create_from_dir(&config, base_dir.path(), &env::temp_dir()) + let toolchain = Toolchain::create_from_dir(base_dir.path(), &env::temp_dir(), &config) .await .unwrap(); - (toolchain.get_pnpm().unwrap().to_owned(), base_dir) + (toolchain, base_dir) } #[tokio::test] async fn generates_paths() { - let (pnpm, temp_dir) = create_pnpm_tool().await; + let (toolchain, temp_dir) = create_pnpm_tool().await; + let pnpm = toolchain.get_node().get_pnpm().unwrap(); assert!(predicates::str::ends_with( PathBuf::from(".moon") @@ -35,18 +36,13 @@ async fn generates_paths() { .to_str() .unwrap() ) - .eval(pnpm.get_install_dir().to_str().unwrap())); + .eval(pnpm.get_install_dir().unwrap().to_str().unwrap())); - let mut bin_path = PathBuf::from(".moon") + let bin_path = PathBuf::from(".moon") .join("tools") .join("node") - .join("1.0.0"); - - if env::consts::OS == "windows" { - bin_path = bin_path.join("pnpm.cmd"); - } else { - bin_path = bin_path.join("bin").join("pnpm"); - } + .join("1.0.0") + .join(get_bin_name_suffix("pnpm", "cmd", false)); assert!(predicates::str::ends_with(bin_path.to_str().unwrap()) .eval(pnpm.get_bin_path().to_str().unwrap())); diff --git a/crates/toolchain/tests/toolchain_test.rs b/crates/toolchain/tests/toolchain_test.rs index 27598d6b3f0..d64be9a3a4b 100644 --- a/crates/toolchain/tests/toolchain_test.rs +++ b/crates/toolchain/tests/toolchain_test.rs @@ -9,7 +9,7 @@ async fn create_toolchain(base_dir: &Path) -> Toolchain { config.node.version = String::from("1.0.0"); - Toolchain::create_from_dir(&config, base_dir, &env::temp_dir()) + Toolchain::create_from_dir(base_dir, &env::temp_dir(), &config) .await .unwrap() } @@ -51,17 +51,3 @@ async fn creates_dirs() { base_dir.close().unwrap(); } - -// #[test] -// fn loads_node_npm() { -// let base_dir = assert_fs::TempDir::new().unwrap(); -// let toolchain = create_toolchain(&base_dir).await; - -// assert_ne!(toolchain.get_node(), None); -// assert_ne!(toolchain.get_npm(), None); -// assert_eq!(toolchain.get_pnpm(), None); -// assert_eq!(toolchain.get_yarn(), None); -// - -// base_dir.close().unwrap(); -// } diff --git a/crates/toolchain/tests/yarn_test.rs b/crates/toolchain/tests/yarn_test.rs index 28a5948ca78..3fe239f9f39 100644 --- a/crates/toolchain/tests/yarn_test.rs +++ b/crates/toolchain/tests/yarn_test.rs @@ -1,11 +1,11 @@ use moon_config::{PackageManager, WorkspaceConfig, YarnConfig}; -use moon_toolchain::tools::yarn::YarnTool; -use moon_toolchain::{Tool, Toolchain}; +use moon_toolchain::helpers::get_bin_name_suffix; +use moon_toolchain::{Executable, Installable, Toolchain}; use predicates::prelude::*; use std::env; use std::path::PathBuf; -async fn create_yarn_tool() -> (YarnTool, assert_fs::TempDir) { +async fn create_yarn_tool() -> (Toolchain, assert_fs::TempDir) { let base_dir = assert_fs::TempDir::new().unwrap(); let mut config = WorkspaceConfig::default(); @@ -16,16 +16,17 @@ async fn create_yarn_tool() -> (YarnTool, assert_fs::TempDir) { version: String::from("6.0.0"), }); - let toolchain = Toolchain::create_from_dir(&config, base_dir.path(), &env::temp_dir()) + let toolchain = Toolchain::create_from_dir(base_dir.path(), &env::temp_dir(), &config) .await .unwrap(); - (toolchain.get_yarn().unwrap().to_owned(), base_dir) + (toolchain, base_dir) } #[tokio::test] async fn generates_paths() { - let (yarn, temp_dir) = create_yarn_tool().await; + let (toolchain, temp_dir) = create_yarn_tool().await; + let yarn = toolchain.get_node().get_yarn().unwrap(); assert!(predicates::str::ends_with( PathBuf::from(".moon") @@ -35,18 +36,13 @@ async fn generates_paths() { .to_str() .unwrap() ) - .eval(yarn.get_install_dir().to_str().unwrap())); + .eval(yarn.get_install_dir().unwrap().to_str().unwrap())); - let mut bin_path = PathBuf::from(".moon") + let bin_path = PathBuf::from(".moon") .join("tools") .join("node") - .join("1.0.0"); - - if env::consts::OS == "windows" { - bin_path = bin_path.join("yarn.cmd"); - } else { - bin_path = bin_path.join("bin").join("yarn"); - } + .join("1.0.0") + .join(get_bin_name_suffix("yarn", "cmd", false)); assert!(predicates::str::ends_with(bin_path.to_str().unwrap()) .eval(yarn.get_bin_path().to_str().unwrap())); diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 2b4a28da6fa..4ff8f1de64b 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -6,7 +6,10 @@ edition = "2021" [dependencies] moon_error = { path = "../error" } moon_logger = { path = "../logger" } +assert_cmd = "2.0" +assert_fs = "1.0" async-recursion = "1.0" +cached = "0.34" chrono = "0.4.19" chrono-humanize = "0.2.1" dirs = "4.0" @@ -18,3 +21,4 @@ regex = "1.5" serde = "1.0" serde_json = { version = "1.0", features = ["preserve_order"] } tokio = { version = "1.16", features = ["full"] } + diff --git a/crates/utils/src/fs.rs b/crates/utils/src/fs.rs index 513e5ff4a8d..b5073525ba1 100644 --- a/crates/utils/src/fs.rs +++ b/crates/utils/src/fs.rs @@ -2,7 +2,6 @@ use async_recursion::async_recursion; use globset::GlobSet; use json_comments::StripComments; use moon_error::{map_io_to_fs_error, map_json_to_error, MoonError}; -use path_clean::PathClean; use regex::Regex; use serde::de::DeserializeOwned; use serde::Serialize; @@ -10,8 +9,6 @@ use std::io::Read; use std::path::{Path, PathBuf}; use tokio::fs; -pub use dirs::home_dir as get_home_dir; - pub fn clean_json(json: String) -> Result { // Remove comments let mut stripped = String::with_capacity(json.len()); @@ -38,61 +35,17 @@ pub async fn create_dir_all(path: &Path) -> Result<(), MoonError> { Ok(()) } -/// If a file starts with "/", expand from the workspace root, otherwise the project root. -pub fn expand_root_path(file: &str, workspace_root: &Path, project_root: &Path) -> PathBuf { - if file.starts_with('/') { - workspace_root.join(file.strip_prefix('/').unwrap()) - } else { - project_root.join(file) - } -} - -// This is not very exhaustive and may be inaccurate. -pub fn is_glob(value: &str) -> bool { - let single_values = vec!['*', '?', '1']; - let paired_values = vec![('{', '}'), ('[', ']')]; - let mut bytes = value.bytes(); - let mut is_escaped = |index: usize| { - if index == 0 { - return false; - } - - bytes.nth(index - 1).unwrap_or(b' ') == b'\\' - }; - - if value.contains("**") { - return true; - } - - for single in single_values { - if !value.contains(single) { - continue; - } +pub fn find_upwards(name: &str, dir: &Path) -> Option { + let findable = dir.join(name); - if let Some(index) = value.find(single) { - if !is_escaped(index) { - return true; - } - } + if findable.exists() { + return Some(findable); } - for (open, close) in paired_values { - if !value.contains(open) || !value.contains(close) { - continue; - } - - if let Some(index) = value.find(open) { - if !is_escaped(index) { - return true; - } - } + match dir.parent() { + Some(parent_dir) => find_upwards(name, parent_dir), + None => None, } - - false -} - -pub fn is_path_glob(path: &Path) -> bool { - is_glob(&path.to_string_lossy()) } pub async fn link_file(from_root: &Path, from: &Path, to_root: &Path) -> Result<(), MoonError> { @@ -149,6 +102,8 @@ pub fn matches_globset(globset: &GlobSet, path: &Path) -> bool { // https://github.com/BurntSushi/ripgrep/issues/2001 #[cfg(windows)] pub fn matches_globset(globset: &GlobSet, path: &Path) -> bool { + use crate::path::normalize_glob; + globset.is_match(&PathBuf::from(normalize_glob(path))) } @@ -158,43 +113,6 @@ pub async fn metadata(path: &Path) -> Result { .map_err(|e| map_io_to_fs_error(e, path.to_path_buf())) } -pub fn normalize(path: &Path) -> PathBuf { - path.to_path_buf().clean() -} - -pub fn normalize_glob(path: &Path) -> String { - // Always use forward slashes for globs - let glob = standardize_separators(&path.to_string_lossy()); - - // Remove UNC prefix as it breaks glob matching - if std::env::consts::OS == "windows" { - return glob.replace("//?/", ""); - } - - glob -} - -#[cfg(not(windows))] -pub fn normalize_separators(path: &str) -> String { - String::from(path) -} - -#[cfg(windows)] -pub fn normalize_separators(path: &str) -> String { - path.replace('/', "\\") -} - -pub fn path_to_string(path: &Path) -> Result { - match path.to_str() { - Some(p) => Ok(p.to_owned()), - None => Err(MoonError::PathInvalidUTF8(path.to_path_buf())), - } -} - -pub fn standardize_separators(path: &str) -> String { - path.replace('\\', "/") -} - pub async fn read_dir(path: &Path) -> Result, MoonError> { let handle_error = |e| map_io_to_fs_error(e, path.to_path_buf()); @@ -292,40 +210,3 @@ where Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - mod is_glob { - use super::*; - - #[test] - fn returns_true_when_a_glob() { - assert!(is_glob("**")); - assert!(is_glob("**/src/*")); - assert!(is_glob("src/**")); - assert!(is_glob("*.ts")); - assert!(is_glob("file.*")); - assert!(is_glob("file.{js,ts}")); - assert!(is_glob("file.[jstx]")); - assert!(is_glob("file.tsx?")); - } - - #[test] - fn returns_false_when_not_glob() { - assert!(!is_glob("dir")); - assert!(!is_glob("file.rs")); - assert!(!is_glob("dir/file.ts")); - assert!(!is_glob("dir/dir/file_test.rs")); - assert!(!is_glob("dir/dirDir/file-ts.js")); - } - - #[test] - fn returns_false_when_escaped_glob() { - assert!(!is_glob("\\*.rs")); - assert!(!is_glob("file\\?.js")); - assert!(!is_glob("folder-\\[id\\]")); - } - } -} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index ac36459614c..b7e81c578a6 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -1,10 +1,14 @@ pub mod fs; +pub mod path; pub mod process; pub mod regex; pub mod test; pub mod time; +use cached::proc_macro::cached; use std::env; +use std::net::{Shutdown, SocketAddr, TcpStream}; +use std::time::Duration; #[macro_export] macro_rules! string_vec { @@ -21,3 +25,17 @@ macro_rules! string_vec { pub fn is_ci() -> bool { env::var("CI").is_ok() } + +#[cached(time = 60)] +pub fn is_offline() -> bool { + // Cloudflare's DNS: https://1.1.1.1/dns/ + let address = SocketAddr::from(([1, 1, 1, 1], 53)); + let mut offline = true; + + if let Ok(stream) = TcpStream::connect_timeout(&address, Duration::new(3, 0)) { + stream.shutdown(Shutdown::Both).unwrap(); + offline = false; + } + + offline +} diff --git a/crates/utils/src/path.rs b/crates/utils/src/path.rs new file mode 100644 index 00000000000..4429ad94f64 --- /dev/null +++ b/crates/utils/src/path.rs @@ -0,0 +1,148 @@ +pub use dirs::home_dir as get_home_dir; +use moon_error::MoonError; +use path_clean::PathClean; +use std::path::{Path, PathBuf}; + +/// If a file starts with "/", expand from the workspace root, otherwise the project root. +pub fn expand_root_path(file: &str, workspace_root: &Path, project_root: &Path) -> PathBuf { + if file.starts_with('/') { + workspace_root.join(file.strip_prefix('/').unwrap()) + } else { + project_root.join(file) + } +} + +// This is not very exhaustive and may be inaccurate. +pub fn is_glob(value: &str) -> bool { + let single_values = vec!['*', '?', '1']; + let paired_values = vec![('{', '}'), ('[', ']')]; + let mut bytes = value.bytes(); + let mut is_escaped = |index: usize| { + if index == 0 { + return false; + } + + bytes.nth(index - 1).unwrap_or(b' ') == b'\\' + }; + + if value.contains("**") { + return true; + } + + for single in single_values { + if !value.contains(single) { + continue; + } + + if let Some(index) = value.find(single) { + if !is_escaped(index) { + return true; + } + } + } + + for (open, close) in paired_values { + if !value.contains(open) || !value.contains(close) { + continue; + } + + if let Some(index) = value.find(open) { + if !is_escaped(index) { + return true; + } + } + } + + false +} + +pub fn is_path_glob(path: &Path) -> bool { + is_glob(&path.to_string_lossy()) +} + +pub fn normalize(path: &Path) -> PathBuf { + path.to_path_buf().clean() +} + +pub fn normalize_glob(path: &Path) -> String { + // Always use forward slashes for globs + let glob = standardize_separators(&path.to_string_lossy()); + + // Remove UNC prefix as it breaks glob matching + if cfg!(windows) { + return glob.replace("//?/", ""); + } + + glob +} + +#[cfg(not(windows))] +pub fn normalize_separators(path: &str) -> String { + path.replace('\\', "/") +} + +#[cfg(windows)] +pub fn normalize_separators(path: &str) -> String { + path.replace('/', "\\") +} + +pub fn path_to_string(path: &Path) -> Result { + match path.to_str() { + Some(p) => Ok(p.to_owned()), + None => Err(MoonError::PathInvalidUTF8(path.to_path_buf())), + } +} + +pub fn replace_home_dir(value: &str) -> String { + if let Some(home_dir) = get_home_dir() { + let home_dir_str = home_dir.to_str().unwrap(); + + // Replace both forward and backward slashes + return value + .replace(home_dir_str, "~") + .replace(&standardize_separators(home_dir_str), "~"); + } + + value.to_owned() +} + +pub fn standardize_separators(path: &str) -> String { + path.replace('\\', "/") +} + +#[cfg(test)] +mod tests { + use super::*; + + mod is_glob { + use super::*; + + #[test] + fn returns_true_when_a_glob() { + assert!(is_glob("**")); + assert!(is_glob("**/src/*")); + assert!(is_glob("src/**")); + assert!(is_glob("*.ts")); + assert!(is_glob("file.*")); + assert!(is_glob("file.{js,ts}")); + assert!(is_glob("file.[jstx]")); + assert!(is_glob("file.tsx?")); + } + + #[test] + fn returns_false_when_not_glob() { + assert!(!is_glob("dir")); + assert!(!is_glob("file.rs")); + assert!(!is_glob("dir/file.ts")); + assert!(!is_glob("dir/dir/file_test.rs")); + assert!(!is_glob("dir/dirDir/file-ts.js")); + } + + #[test] + fn returns_false_when_escaped_glob() { + assert!(!is_glob("\\*.rs")); + assert!(!is_glob("file\\?.js")); + assert!(!is_glob("folder-\\[id\\]")); + } + } +} diff --git a/crates/utils/src/process.rs b/crates/utils/src/process.rs index c20e8d683e0..aaec5acc052 100644 --- a/crates/utils/src/process.rs +++ b/crates/utils/src/process.rs @@ -1,191 +1,327 @@ -use crate::fs::get_home_dir; +use crate::path; use moon_error::{map_io_to_process_error, MoonError}; use moon_logger::{color, logging_enabled, trace}; use std::env; use std::ffi::OsStr; +use std::path::Path; use std::sync::Arc; -use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::Command; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::process::Command as TokioCommand; use tokio::sync::RwLock; use tokio::task; -pub use std::process::{Output, Stdio}; +pub use std::process::{ExitStatus, Output, Stdio}; + +// Based on how Node.js executes Windows commands: +// https://github.com/nodejs/node/blob/master/lib/child_process.js#L572 +fn create_windows_cmd() -> TokioCommand { + let mut cmd = TokioCommand::new("cmd.exe"); + cmd.arg("/d"); + cmd.arg("/s"); + cmd.arg("/q"); // Hide the script from echoing in the output + cmd.arg("/c"); + cmd +} -fn log_command_info(command: &Command) { - // Avoid all this overhead if we're not logging - if !logging_enabled() { - return; - } +pub fn is_windows_script(bin: &str) -> bool { + bin.ends_with(".cmd") || bin.ends_with(".bat") +} - let cmd = command.as_std(); - let bin_name = cmd.get_program().to_str().unwrap_or(""); - let args_list = cmd - .get_args() - .into_iter() - .map(|a| a.to_str().unwrap()) - .collect::>(); - let command_line = format!("{} {}", bin_name, args_list.join(" ")) - .replace(get_home_dir().unwrap_or_default().to_str().unwrap(), "~"); - - if let Some(cwd) = cmd.get_current_dir() { - trace!( - target: "moon:utils", - "Running command {} (in {})", - color::shell(&command_line), - color::path(cwd), - ); - } else { - trace!( - target: "moon:utils", - "Running command {} ", - color::shell(&command_line), - ); - } +pub fn output_to_string(data: &[u8]) -> String { + String::from_utf8(data.to_vec()).unwrap_or_default() } -#[cfg(not(windows))] -pub fn create_command>(bin: S) -> Command { - Command::new(bin) +pub fn output_to_trimmed_string(data: &[u8]) -> String { + output_to_string(data).trim().to_owned() } -#[cfg(windows)] -pub fn create_command>(bin: S) -> Command { - let bin_name = bin.as_ref().to_str().unwrap_or_default(); - - // Based on how Node.js executes Windows commands: - // https://github.com/nodejs/node/blob/master/lib/child_process.js#L572 - if bin_name.ends_with(".cmd") || bin_name.ends_with(".bat") { - let mut cmd = Command::new("cmd.exe"); - cmd.arg("/d"); - cmd.arg("/s"); - cmd.arg("/c"); - cmd.arg(bin); - cmd - } else { - Command::new(bin) - } +pub struct Command { + bin: String, + + cmd: TokioCommand, + + /// Convert non-zero exits to errors. + error: bool, } -pub async fn exec_command(command: &mut Command) -> Result { - log_command_info(command); +// This is rather annoying that we have to re-implement all these methods, +// but the encapsulation this struct provides is necessary. +impl Command { + pub fn new>(bin: S) -> Self { + let mut bin_name = String::from(bin.as_ref().to_string_lossy()); + let mut cmd; + + // Referencing cmd.exe directly + if bin_name == "cmd" || bin_name == "cmd.exe" { + bin_name = String::from("cmd.exe"); + cmd = create_windows_cmd(); + + // Referencing a batch script that needs to be ran with cmd.exe + } else if is_windows_script(&bin_name) { + bin_name = String::from("cmd.exe"); + cmd = create_windows_cmd(); + cmd.arg(bin); + + // Assume a command exists on the system + } else { + cmd = TokioCommand::new(bin); + } + + Command { + bin: bin_name, + cmd, + error: true, + } + } + + pub fn arg>(&mut self, arg: S) -> &mut Command { + self.cmd.arg(arg); + self + } - let output = command.output(); - let output = output.await.map_err(|e| { - map_io_to_process_error(e, command.as_std().get_program().to_str().unwrap()) - })?; + pub fn args(&mut self, args: I) -> &mut Command + where + I: IntoIterator, + S: AsRef, + { + self.cmd.args(args); + self + } - handle_nonzero_status(command, &output)?; + pub fn cwd>(&mut self, dir: P) -> &mut Command { + self.cmd.current_dir(dir); + self + } - Ok(output) -} + pub fn env(&mut self, key: K, val: V) -> &mut Command + where + K: AsRef, + V: AsRef, + { + self.cmd.env(key, val); + self + } -pub async fn exec_command_capture_stderr(command: &mut Command) -> Result { - let output = exec_command(command).await?; + pub fn envs(&mut self, vars: I) -> &mut Command + where + I: IntoIterator, + K: AsRef, + V: AsRef, + { + self.cmd.envs(vars); + self + } - Ok(output_to_string(&output.stderr)) -} + pub async fn exec_capture_output(&mut self) -> Result { + self.log_command_info(None); -pub async fn exec_command_capture_stdout(command: &mut Command) -> Result { - let output = exec_command(command).await?; + let output = self.cmd.output(); + let output = output + .await + .map_err(|e| map_io_to_process_error(e, &self.bin))?; - Ok(output_to_string(&output.stdout)) -} + self.handle_nonzero_status(&output)?; + + Ok(output) + } + + pub async fn exec_capture_output_with_input( + &mut self, + input: &str, + ) -> Result { + self.log_command_info(Some(input)); + + let mut child = self + .cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| map_io_to_process_error(e, &self.bin))?; -pub async fn spawn_command(command: &mut Command) -> Result { - log_command_info(command); - - let mut child = command - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .envs(env::vars()) - // Inherit ANSI colors since they're stripped from pipes - .env("FORCE_COLOR", env::var("FORCE_COLOR").unwrap_or_default()) - .env("TERM", env::var("TERM").unwrap_or_default()) - .spawn() - .unwrap(); - - // We need to log the child process output to the parent terminal - // AND capture stdout/stderr so that we can cache it for future runs. - // This doesn't seem to be supported natively by `Stdio`, so I have - // this *real ugly* implementation to solve it. There's gotta be a - // better way to do this? - // https://stackoverflow.com/a/49063262 - let err = BufReader::new(child.stderr.take().unwrap()); - let out = BufReader::new(child.stdout.take().unwrap()); - - // Spawn additional threads for logging the buffer - let stderr = Arc::new(RwLock::new(vec![])); - let stdout = Arc::new(RwLock::new(vec![])); - let stderr_clone = Arc::clone(&stderr); - let stdout_clone = Arc::clone(&stdout); - - task::spawn(async move { - let mut lines = err.lines(); - let mut stderr_write = stderr_clone.write().await; - - while let Some(line) = lines.next_line().await.unwrap() { - eprintln!("{}", line); - stderr_write.push(line); + let mut stdin = child.stdin.take().unwrap(); + stdin.write_all(input.as_bytes()).await.unwrap(); + drop(stdin); + + let output = child + .wait_with_output() + .await + .map_err(|e| map_io_to_process_error(e, &self.bin))?; + + self.handle_nonzero_status(&output)?; + + Ok(output) + } + + pub async fn exec_stream_output(&mut self) -> Result { + self.log_command_info(None); + + let status = self + .cmd + .spawn() + .map_err(|e| map_io_to_process_error(e, &self.bin))? + .wait() + .await + .map_err(|e| map_io_to_process_error(e, &self.bin))?; + + if self.error && !status.success() { + return Err(MoonError::ProcessNonZero( + self.bin.clone(), + status.code().unwrap_or(-1), + )); } - }); - task::spawn(async move { - let mut lines = out.lines(); - let mut stdout_write = stdout_clone.write().await; + Ok(status) + } + + pub async fn exec_stream_and_capture_output(&mut self) -> Result { + self.log_command_info(None); + + let mut child = self + .cmd + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .envs(env::vars()) + // Inherit ANSI colors since they're stripped from pipes + .env("FORCE_COLOR", env::var("FORCE_COLOR").unwrap_or_default()) + .env("TERM", env::var("TERM").unwrap_or_default()) + .spawn() + .map_err(|e| map_io_to_process_error(e, &self.bin))?; + + // We need to log the child process output to the parent terminal + // AND capture stdout/stderr so that we can cache it for future runs. + // This doesn't seem to be supported natively by `Stdio`, so I have + // this *real ugly* implementation to solve it. There's gotta be a + // better way to do this? + // https://stackoverflow.com/a/49063262 + let err = BufReader::new(child.stderr.take().unwrap()); + let out = BufReader::new(child.stdout.take().unwrap()); + + // Spawn additional threads for logging the buffer + let stderr = Arc::new(RwLock::new(vec![])); + let stdout = Arc::new(RwLock::new(vec![])); + let stderr_clone = Arc::clone(&stderr); + let stdout_clone = Arc::clone(&stdout); + + task::spawn(async move { + let mut lines = err.lines(); + let mut stderr_write = stderr_clone.write().await; + + while let Some(line) = lines.next_line().await.unwrap() { + eprintln!("{}", line); + stderr_write.push(line); + } + }); + + task::spawn(async move { + let mut lines = out.lines(); + let mut stdout_write = stdout_clone.write().await; + + while let Some(line) = lines.next_line().await.unwrap() { + println!("{}", line); + stdout_write.push(line); + } + }); + + // Attempt to capture the child output + let mut output = child + .wait_with_output() + .await + .map_err(|e| map_io_to_process_error(e, &self.bin))?; + + if output.stderr.is_empty() { + output.stderr = stderr.read().await.join("").into_bytes(); + } - while let Some(line) = lines.next_line().await.unwrap() { - println!("{}", line); - stdout_write.push(line); + if output.stdout.is_empty() { + output.stdout = stdout.read().await.join("").into_bytes(); } - }); - // Attempt to capture the child output - let mut output = child.wait_with_output().await.map_err(|e| { - map_io_to_process_error(e, command.as_std().get_program().to_str().unwrap()) - })?; + self.handle_nonzero_status(&output)?; - if output.stderr.is_empty() { - output.stderr = stderr.read().await.join("").into_bytes(); + Ok(output) } - if output.stdout.is_empty() { - output.stdout = stdout.read().await.join("").into_bytes(); + pub fn no_error_on_failure(&mut self) -> &mut Command { + self.error = false; + self } - handle_nonzero_status(command, &output)?; + pub fn output_to_error(&self, output: &Output, with_message: bool) -> MoonError { + let code = output.status.code().unwrap_or(-1); - Ok(output) -} + if !with_message { + return MoonError::ProcessNonZero(self.bin.clone(), code); + } -pub fn output_to_string(data: &[u8]) -> String { - String::from_utf8(data.to_vec()).unwrap_or_default() -} + let message = output_to_trimmed_string(&output.stderr); + + MoonError::ProcessNonZeroWithOutput(self.bin.clone(), code, message) + } -fn handle_nonzero_status(command: &mut Command, output: &Output) -> Result<(), MoonError> { - if !output.status.success() { - let bin_name = command - .as_std() - .get_program() - .to_str() - .unwrap_or(""); - - match output.status.code() { - Some(code) => { - return Err(MoonError::ProcessNonZero( - bin_name.to_owned(), - code, - output_to_string(&output.stderr), // Always correct? + fn handle_nonzero_status(&self, output: &Output) -> Result<(), MoonError> { + if self.error && !output.status.success() { + return Err(self.output_to_error(output, true)); + } + + Ok(()) + } + + fn log_command_info(&self, input: Option<&str>) { + // Avoid all this overhead if we're not logging + if !logging_enabled() { + return; + } + + let cmd = &self.cmd.as_std(); + let args = cmd + .get_args() + .into_iter() + .map(|a| a.to_str().unwrap()) + .collect::>(); + let mut command_line = path::replace_home_dir(&format!("{} {}", self.bin, args.join(" "))); + + if input.is_some() { + command_line = format!( + "{} {} {}", + color::muted_light(&input.unwrap().replace('\n', " ")), + color::muted(">"), + color::shell(&command_line) + ); + } else { + command_line = color::shell(&command_line); + } + + let mut envs_list = vec![]; + + for (key, value) in cmd.get_envs() { + if value.is_some() { + let key_str = key.to_str().unwrap(); + + // This is very noisy, maybe with a verbose logging setting? + if key_str == "PATH" { + continue; + } + + envs_list.push(format!( + "\n {}{}{}", + key_str, + color::muted("="), + color::muted_light(value.unwrap().to_str().unwrap()) )); } - None => { - return Err(MoonError::ProcessNonZero( - bin_name.to_owned(), - -1, - String::from("Process terminated by signal."), - )) - } - }; - } + } - Ok(()) + trace!( + target: "moon:utils", + "Running command {} (in {}){}", + command_line, + if let Some(cwd) = cmd.get_current_dir() { + color::path(cwd) + } else { + String::from("working dir") + }, + envs_list.join("") + ); + } } diff --git a/crates/utils/src/test.rs b/crates/utils/src/test.rs index 6b5a831ad92..63abe9f6725 100644 --- a/crates/utils/src/test.rs +++ b/crates/utils/src/test.rs @@ -1,7 +1,19 @@ -use crate::fs; +use crate::path; use std::env; use std::path::{Path, PathBuf}; +pub fn create_fixtures_sandbox(dir: &str) -> assert_fs::fixture::TempDir { + use assert_fs::prelude::*; + + let temp_dir = assert_fs::fixture::TempDir::new().unwrap(); + + temp_dir + .copy_from(get_fixtures_dir(dir), &["**/*"]) + .unwrap(); + + temp_dir +} + pub fn get_fixtures_dir(dir: &str) -> PathBuf { get_fixtures_root().join(dir) } @@ -13,7 +25,50 @@ pub fn get_fixtures_root() -> PathBuf { path.canonicalize().unwrap() } +pub fn replace_fixtures_dir(value: &str, dir: &Path) -> String { + let dir_str = dir.to_str().unwrap(); + + // Replace both forward and backward slashes + value + .replace(dir_str, "") + .replace(&path::standardize_separators(dir_str), "") +} + // We need to do this so slashes are accurate and always forward pub fn wrap_glob(path: &Path) -> PathBuf { - PathBuf::from(fs::normalize_glob(path)) + PathBuf::from(path::normalize_glob(path)) +} + +pub fn create_moon_command(fixture: &str) -> assert_cmd::Command { + let mut cmd = create_moon_command_in(&get_fixtures_dir(fixture)); + // Never cache in these tests since they're not in a sandbox + cmd.env("MOON_CACHE", "off"); + cmd +} + +pub fn create_moon_command_in(path: &Path) -> assert_cmd::Command { + let mut cmd = assert_cmd::Command::cargo_bin("moon").unwrap(); + cmd.current_dir(path); + // Let our code know were running tests + cmd.env("MOON_TEST", "true"); + // Hide install output as it disrupts testing snapshots + cmd.env("MOON_TEST_HIDE_INSTALL_OUTPUT", "true"); + // Standardize file system paths for testing snapshots + cmd.env("MOON_TEST_STANDARDIZE_PATHS", "true"); + // Uncomment for debugging + // cmd.arg("--logLevel"); + // cmd.arg("trace"); + cmd +} + +pub fn get_assert_output(assert: &assert_cmd::assert::Assert) -> String { + get_assert_stdout_output(assert) + &get_assert_stderr_output(assert) +} + +pub fn get_assert_stderr_output(assert: &assert_cmd::assert::Assert) -> String { + String::from_utf8(assert.get_output().stderr.to_owned()).unwrap() +} + +pub fn get_assert_stdout_output(assert: &assert_cmd::assert::Assert) -> String { + String::from_utf8(assert.get_output().stdout.to_owned()).unwrap() } diff --git a/crates/workspace/src/action.rs b/crates/workspace/src/action.rs index 89a5bbf8f1e..24e7e146323 100644 --- a/crates/workspace/src/action.rs +++ b/crates/workspace/src/action.rs @@ -5,6 +5,7 @@ pub enum ActionStatus { Cached, // CachedFromRemote, // TODO Failed, + FailedAndAbort, Invalid, Passed, Running, @@ -16,8 +17,6 @@ pub struct Action { pub error: Option, - pub exit_code: i8, - pub label: Option, pub node_index: NodeIndex, @@ -25,10 +24,6 @@ pub struct Action { pub start_time: Instant, pub status: ActionStatus, - - pub stderr: String, - - pub stdout: String, } impl Action { @@ -36,19 +31,15 @@ impl Action { Action { duration: None, error: None, - exit_code: -1, label: None, node_index, start_time: Instant::now(), status: ActionStatus::Running, - stderr: String::new(), - stdout: String::new(), } } - pub fn pass(&mut self, status: ActionStatus) { - self.status = status; - self.duration = Some(self.start_time.elapsed()); + pub fn abort(&mut self) { + self.status = ActionStatus::FailedAndAbort; } pub fn fail(&mut self, error: String) { @@ -56,4 +47,18 @@ impl Action { self.status = ActionStatus::Failed; self.duration = Some(self.start_time.elapsed()); } + + pub fn has_failed(&self) -> bool { + matches!(self.status, ActionStatus::Failed) + || matches!(self.status, ActionStatus::FailedAndAbort) + } + + pub fn pass(&mut self, status: ActionStatus) { + self.status = status; + self.duration = Some(self.start_time.elapsed()); + } + + pub fn should_abort(&self) -> bool { + matches!(self.status, ActionStatus::FailedAndAbort) + } } diff --git a/crates/workspace/src/action_runner.rs b/crates/workspace/src/action_runner.rs index 58229e34649..d81e1dcd08e 100644 --- a/crates/workspace/src/action_runner.rs +++ b/crates/workspace/src/action_runner.rs @@ -3,7 +3,7 @@ use crate::actions::{install_node_deps, run_target, setup_toolchain, sync_projec use crate::dep_graph::{DepGraph, Node}; use crate::errors::WorkspaceError; use crate::workspace::Workspace; -use moon_logger::{color, debug, trace}; +use moon_logger::{color, debug, error, trace}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::RwLock; @@ -13,20 +13,37 @@ const TARGET: &str = "moon:action-runner"; async fn run_action( workspace: Arc>, + action: &mut Action, action_node: &Node, primary_target: &str, passthrough_args: &[String], -) -> Result { - let status = match action_node { - Node::InstallNodeDeps => install_node_deps(workspace).await?, +) -> Result<(), WorkspaceError> { + let result = match action_node { + Node::InstallNodeDeps => install_node_deps(workspace).await, Node::RunTarget(target_id) => { - run_target(workspace, target_id, primary_target, passthrough_args).await? + run_target(workspace, target_id, primary_target, passthrough_args).await } - Node::SetupToolchain => setup_toolchain(workspace).await?, - Node::SyncProject(project_id) => sync_project(workspace, project_id).await?, + Node::SetupToolchain => setup_toolchain(workspace).await, + Node::SyncProject(project_id) => sync_project(workspace, project_id).await, }; - Ok(status) + match result { + Ok(status) => { + action.pass(status); + } + Err(error) => { + action.fail(error.to_string()); + + // If these fail, we should abort instead of trying to continue + if matches!(action_node, Node::SetupToolchain) + || matches!(action_node, Node::InstallNodeDeps) + { + action.abort(); + } + } + } + + Ok(()) } pub struct ActionRunner { @@ -92,10 +109,11 @@ impl ActionRunner { for (b, batch) in batches.into_iter().enumerate() { let batch_count = b + 1; + let batch_target_name = format!("{}:batch:{}", TARGET, batch_count); let actions_count = batch.len(); trace!( - target: &format!("{}:batch:{}", TARGET, batch_count), + target: &batch_target_name, "Running {} actions", actions_count ); @@ -110,11 +128,11 @@ impl ActionRunner { let primary_target_clone = Arc::clone(&primary_target); action_handles.push(task::spawn(async move { - let mut result = Action::new(node_index); + let mut action = Action::new(node_index); let own_graph = graph_clone.read().await; if let Some(node) = own_graph.get_node_from_index(node_index) { - result.label = Some(node.label()); + action.label = Some(node.label()); let log_target_name = format!("{}:batch:{}:{}", TARGET, batch_count, action_count); @@ -126,42 +144,37 @@ impl ActionRunner { log_action_label ); - match run_action( + run_action( workspace_clone, + &mut action, node, &primary_target_clone, &passthrough_args_clone, ) - .await - { - Ok(status) => { - result.pass(status); - - trace!( - target: &log_target_name, - "Ran action {} in {:?}", - log_action_label, - result.duration.unwrap() - ); - } - Err(error) => { - result.fail(error.to_string()); - - trace!( - target: &log_target_name, - "Action {} failed in {:?}", - log_action_label, - result.duration.unwrap() - ); - } - }; + .await?; + + if action.has_failed() { + trace!( + target: &log_target_name, + "Failed to run action {} in {:?}", + log_action_label, + action.duration.unwrap() + ); + } else { + trace!( + target: &log_target_name, + "Ran action {} in {:?}", + log_action_label, + action.duration.unwrap() + ); + } } else { - result.status = ActionStatus::Invalid; + action.status = ActionStatus::Invalid; return Err(WorkspaceError::DepGraphUnknownNode(node_index.index())); } - Ok(result) + Ok(action) })); } @@ -170,7 +183,14 @@ impl ActionRunner { for handle in action_handles { match handle.await { Ok(Ok(result)) => { - if self.bail && result.error.is_some() { + if result.should_abort() { + error!( + target: &batch_target_name, + "Encountered a critical error, aborting the action runner" + ); + } + + if self.bail && result.error.is_some() || result.should_abort() { return Err(WorkspaceError::ActionRunnerFailure(result.error.unwrap())); } diff --git a/crates/workspace/src/actions/hashing/target.rs b/crates/workspace/src/actions/hashing/target.rs index 9cb1383dd0d..ca48fde6526 100644 --- a/crates/workspace/src/actions/hashing/target.rs +++ b/crates/workspace/src/actions/hashing/target.rs @@ -2,6 +2,7 @@ use crate::{Workspace, WorkspaceError}; use moon_cache::Hasher; use moon_project::{ExpandedFiles, Project, Task}; use moon_utils::fs; +use moon_utils::path::path_to_string; use std::path::Path; fn convert_paths_to_strings( @@ -21,7 +22,7 @@ fn convert_paths_to_strings( path }; - files.push(fs::path_to_string(rel_path)?); + files.push(path_to_string(rel_path)?); } } @@ -34,19 +35,17 @@ pub async fn create_target_hasher( task: &Task, ) -> Result { let vcs = workspace.detect_vcs(); + let globset = task.create_globset()?; let mut hasher = Hasher::new(workspace.config.node.version.clone()); hasher.hash_project(project); hasher.hash_task(task); // Hash root configs first - hasher.hash_package_json(&workspace.load_package_json().await?); + hasher.hash_package_json(&workspace.package_json); - if let Some(root_tsconfig) = workspace - .load_tsconfig_json(&workspace.config.typescript.root_config_file_name) - .await? - { - hasher.hash_tsconfig_json(&root_tsconfig); + if let Some(root_tsconfig) = &workspace.tsconfig_json { + hasher.hash_tsconfig_json(root_tsconfig); } // Hash project configs second so they can override @@ -69,13 +68,12 @@ pub async fn create_target_hasher( hasher.hash_inputs(hashed_files); } - // For input globs, its much more performant to: + // For input globs, it's much more performant to: // `git ls-tree` -> match against glob patterns // Then it is to: // glob + walk the file system -> `git hash-object` if !task.input_globs.is_empty() { let mut hashed_file_tree = vcs.get_file_tree_hashes(&project.source).await?; - let globset = task.create_globset()?; // Input globs are absolute paths, so we must do the same hashed_file_tree.retain(|k, _| fs::matches_globset(&globset, &workspace.root.join(k))); @@ -83,7 +81,24 @@ pub async fn create_target_hasher( hasher.hash_inputs(hashed_file_tree); } - // TODO include local file changes + // Include local file changes so that development builds work. + // Also run this LAST as it should take highest precedence! + if vcs.is_enabled() { + let local_files = vcs.get_touched_files().await?; + + if !local_files.all.is_empty() { + // Only hash files that are within the task's inputs + let files = local_files + .all + .into_iter() + .filter(|f| fs::matches_globset(&globset, &workspace.root.join(f))) + .collect::>(); + + if !files.is_empty() { + hasher.hash_inputs(vcs.get_file_hashes(&files).await?); + } + } + } Ok(hasher) } diff --git a/crates/workspace/src/actions/install_node_deps.rs b/crates/workspace/src/actions/install_node_deps.rs index 4b9f24f7f59..947c996e2b6 100644 --- a/crates/workspace/src/actions/install_node_deps.rs +++ b/crates/workspace/src/actions/install_node_deps.rs @@ -1,87 +1,146 @@ use crate::action::ActionStatus; use crate::errors::WorkspaceError; use crate::workspace::Workspace; +use moon_config::PackageManager; use moon_error::map_io_to_fs_error; -use moon_logger::{color, debug}; -use moon_utils::fs; +use moon_logger::{color, debug, warn}; +use moon_utils::{fs, is_offline}; use std::sync::Arc; use tokio::sync::RwLock; const TARGET: &str = "moon:action:install-node-deps"; -pub async fn install_node_deps( - workspace: Arc>, -) -> Result { - let workspace = workspace.read().await; - let toolchain = &workspace.toolchain; - let manager = toolchain.get_node_package_manager(); - let mut cache = workspace.cache.cache_workspace_state().await?; - - // Update artifacts based on node settings - let node_config = &workspace.config.node; - let mut root_package = workspace.load_package_json().await?; - - if node_config.add_engines_constraint && root_package.add_engine("node", &node_config.version) { - root_package.save().await?; - +/// Add `packageManager` to root `package.json`. +fn add_package_manager(workspace: &mut Workspace) -> bool { + let manager_version = match workspace.config.node.package_manager { + PackageManager::Npm => format!("npm@{}", workspace.config.node.npm.version), + PackageManager::Pnpm => format!( + "pnpm@{}", + workspace.config.node.pnpm.as_ref().unwrap().version + ), + PackageManager::Yarn => format!( + "yarn@{}", + workspace.config.node.yarn.as_ref().unwrap().version + ), + }; + + if workspace.toolchain.get_node().is_corepack_aware() + && workspace.package_json.set_package_manager(&manager_version) + { debug!( target: TARGET, - "Adding engines version constraint to root {}", + "Adding package manager version to root {}", color::file("package.json") ); - } - if let Some(version_manager) = &node_config.sync_version_manager_config { - let rc_name = version_manager.get_config_file_name(); - let rc_path = workspace.root.join(&rc_name); + return true; + } - fs::write(&rc_path, &node_config.version).await?; + false +} +/// Add `engines` constraint to root `package.json`. +fn add_engines_constraint(workspace: &mut Workspace) -> bool { + if workspace.config.node.add_engines_constraint + && workspace + .package_json + .add_engine("node", &workspace.config.node.version) + { debug!( target: TARGET, - "Syncing Node.js version to root {}", - color::file(&rc_name) + "Adding engines version constraint to root {}", + color::file("package.json") ); - } - // Get the last modified time of the root lockfile - let lockfile = workspace.root.join(manager.get_lockfile_name()); - let mut last_modified = 0; + return true; + } - if lockfile.exists() { - let lockfile_metadata = fs::metadata(&lockfile).await?; + false +} - last_modified = cache.to_millis( - lockfile_metadata - .modified() - .map_err(|e| map_io_to_fs_error(e, lockfile.clone()))?, - ); +pub async fn install_node_deps( + workspace: Arc>, +) -> Result { + // Writes root `package.json` + { + let mut workspace = workspace.write().await; + let added_manager = add_package_manager(&mut workspace); + let added_engines = add_engines_constraint(&mut workspace); + + if added_manager || added_engines { + workspace.package_json.save().await?; + } } - // Install deps if the lockfile has been modified - // since the last time dependencies were installed! - if last_modified == 0 || last_modified > cache.item.last_node_install_time { - debug!(target: TARGET, "Installing Node.js dependencies",); + // Read only + { + let workspace = workspace.read().await; + let mut cache = workspace.cache.cache_workspace_state().await?; + let manager = workspace.toolchain.get_node().get_package_manager(); + let node_config = &workspace.config.node; + + // Create nvm/nodenv config file + if let Some(version_manager) = &node_config.sync_version_manager_config { + let rc_name = version_manager.get_config_file_name(); + let rc_path = workspace.root.join(&rc_name); + + fs::write(&rc_path, &node_config.version).await?; + + debug!( + target: TARGET, + "Syncing Node.js version to root {}", + color::file(&rc_name) + ); + } - manager.install_dependencies(toolchain).await?; + // Get the last modified time of the root lockfile + let lockfile = workspace.root.join(manager.get_lockfile_name()); + let mut last_modified = 0; - if node_config.dedupe_on_lockfile_change { - debug!(target: TARGET, "Dedupeing dependencies",); + if lockfile.exists() { + let lockfile_metadata = fs::metadata(&lockfile).await?; - manager.dedupe_dependencies(toolchain).await?; + last_modified = cache.to_millis( + lockfile_metadata + .modified() + .map_err(|e| map_io_to_fs_error(e, lockfile.clone()))?, + ); } - // Update the cache with the timestamp - cache.item.last_node_install_time = cache.now_millis(); - cache.save().await?; + // Install deps if the lockfile has been modified + // since the last time dependencies were installed! + if last_modified == 0 || last_modified > cache.item.last_node_install_time { + debug!(target: TARGET, "Installing Node.js dependencies"); - return Ok(ActionStatus::Passed); - } + if is_offline() { + warn!( + target: TARGET, + "No internet connection, assuming offline and skipping install" + ); + + return Ok(ActionStatus::Skipped); + } + + manager.install_dependencies(&workspace.toolchain).await?; + + if node_config.dedupe_on_lockfile_change { + debug!(target: TARGET, "Dedupeing dependencies"); - debug!( - target: TARGET, - "Lockfile has not changed since last install, skipping Node.js dependencies", - ); + manager.dedupe_dependencies(&workspace.toolchain).await?; + } + + // Update the cache with the timestamp + cache.item.last_node_install_time = cache.now_millis(); + cache.save().await?; + + return Ok(ActionStatus::Passed); + } + + debug!( + target: TARGET, + "Lockfile has not changed since last install, skipping Node.js dependencies", + ); + } Ok(ActionStatus::Skipped) } diff --git a/crates/workspace/src/actions/run_target.rs b/crates/workspace/src/actions/run_target.rs index 120742a18a3..b3beb66e101 100644 --- a/crates/workspace/src/actions/run_target.rs +++ b/crates/workspace/src/actions/run_target.rs @@ -7,12 +7,13 @@ use moon_config::TaskType; use moon_logger::{color, debug}; use moon_project::{Project, Target, Task}; use moon_terminal::output::{label_run_target, label_run_target_failed}; -use moon_toolchain::{get_path_env_var, Tool}; -use moon_utils::process::{create_command, exec_command, output_to_string, spawn_command}; -use moon_utils::{fs, string_vec}; +use moon_toolchain::{get_path_env_var, Executable}; +use moon_utils::process::{output_to_string, Command, Output}; +use moon_utils::{is_ci, path, string_vec}; use std::collections::HashMap; +use std::env; +use std::path::Path; use std::sync::Arc; -use tokio::process::Command; use tokio::sync::RwLock; const TARGET: &str = "moon:action:run-target"; @@ -26,26 +27,26 @@ async fn create_env_vars( env_vars.insert( "MOON_CACHE_DIR".to_owned(), - fs::path_to_string(&workspace.cache.dir)?, + path::path_to_string(&workspace.cache.dir)?, ); env_vars.insert("MOON_PROJECT_ID".to_owned(), project.id.clone()); env_vars.insert( "MOON_PROJECT_ROOT".to_owned(), - fs::path_to_string(&project.root)?, + path::path_to_string(&project.root)?, ); env_vars.insert("MOON_PROJECT_SOURCE".to_owned(), project.source.clone()); env_vars.insert("MOON_RUN_TARGET".to_owned(), task.target.clone()); env_vars.insert( "MOON_TOOLCHAIN_DIR".to_owned(), - fs::path_to_string(&workspace.toolchain.dir)?, + path::path_to_string(&workspace.toolchain.dir)?, ); env_vars.insert( "MOON_WORKSPACE_ROOT".to_owned(), - fs::path_to_string(&workspace.root)?, + path::path_to_string(&workspace.root)?, ); env_vars.insert( "MOON_WORKING_DIR".to_owned(), - fs::path_to_string(&workspace.working_dir)?, + path::path_to_string(&workspace.working_dir)?, ); // Store runtime data on the file system so that downstream commands can utilize it @@ -53,7 +54,7 @@ async fn create_env_vars( env_vars.insert( "MOON_PROJECT_RUNFILE".to_owned(), - fs::path_to_string(&runfile.path)?, + path::path_to_string(&runfile.path)?, ); Ok(env_vars) @@ -93,30 +94,29 @@ fn create_node_target_command( args.extend(create_node_options(task)); } "npm" => { - cmd = workspace.toolchain.get_npm().get_bin_path(); + cmd = node.get_npm().get_bin_path(); } "pnpm" => { - cmd = workspace.toolchain.get_pnpm().unwrap().get_bin_path(); + cmd = node.get_pnpm().unwrap().get_bin_path(); } "yarn" => { - cmd = workspace.toolchain.get_yarn().unwrap().get_bin_path(); + cmd = node.get_yarn().unwrap().get_bin_path(); } bin => { let bin_path = node.find_package_bin_path(bin, &project.root)?; args.extend(create_node_options(task)); - args.push(fs::path_to_string(&bin_path)?); + args.push(path::path_to_string(&bin_path)?); } }; // Create the command - let mut command = create_command(cmd); + let mut command = Command::new(cmd); - command - .args(&args) - .args(&task.args) - .envs(&task.env) - .env("PATH", get_path_env_var(node.get_bin_dir())); + command.args(&args).args(&task.args).envs(&task.env).env( + "PATH", + get_path_env_var(node.get_bin_path().parent().unwrap()), + ); Ok(command) } @@ -135,37 +135,50 @@ fn create_node_target_command( let cmd = match task.command.as_str() { "node" => node.get_bin_path().clone(), - "npm" => workspace.toolchain.get_npm().get_bin_path().clone(), - "pnpm" => workspace - .toolchain - .get_pnpm() - .unwrap() - .get_bin_path() - .clone(), - "yarn" => workspace - .toolchain - .get_yarn() - .unwrap() - .get_bin_path() - .clone(), + "npm" => node.get_npm().get_bin_path().clone(), + "pnpm" => node.get_pnpm().unwrap().get_bin_path().clone(), + "yarn" => node.get_yarn().unwrap().get_bin_path().clone(), bin => node.find_package_bin_path(bin, &project.root)?, }; // Create the command - let mut command = create_command(cmd); + let mut command = Command::new(cmd); command .args(&task.args) .envs(&task.env) - .env("PATH", get_path_env_var(node.get_bin_dir())) + .env( + "PATH", + get_path_env_var(node.get_bin_path().parent().unwrap()), + ) .env("NODE_OPTIONS", create_node_options(task).join(" ")); Ok(command) } -fn create_shell_target_command(task: &Task) -> Command { - let mut cmd = create_command(&task.command); - cmd.args(&task.args); +#[cfg(not(windows))] +fn create_system_target_command(task: &Task, _cwd: &Path) -> Command { + let mut cmd = Command::new(&task.command); + cmd.args(&task.args).envs(&task.env); + cmd +} + +#[cfg(windows)] +fn create_system_target_command(task: &Task, cwd: &Path) -> Command { + use moon_utils::process::is_windows_script; + + let mut cmd = Command::new(&task.command); + + for arg in &task.args { + // cmd.exe requires an absolute path to batch files + if is_windows_script(arg) { + cmd.arg(cwd.join(arg)); + } else { + cmd.arg(arg); + } + } + + cmd.envs(&task.env); cmd } @@ -174,20 +187,24 @@ async fn create_target_command( project: &Project, task: &Task, ) -> Result { - let exec_dir = if task.options.run_from_workspace_root { + let working_dir = if task.options.run_from_workspace_root { &workspace.root } else { &project.root }; - let env_vars = create_env_vars(workspace, project, task).await?; - let mut command = match task.type_of { TaskType::Node => create_node_target_command(workspace, project, task)?, - _ => create_shell_target_command(task), + _ => create_system_target_command(task, working_dir), }; - command.current_dir(&exec_dir).envs(env_vars); + let env_vars = create_env_vars(workspace, project, task).await?; + + command + .cwd(working_dir) + .envs(env_vars) + // We need to handle non-zero's manually + .no_error_on_failure(); Ok(command) } @@ -215,7 +232,7 @@ pub async fn run_target( if cache.item.hash == hash { print_target_label(target_id, "(cached)", cache.item.exit_code != 0); - print_cache_item(&cache.item, true); + print_cache_item(&cache.item); return Ok(ActionStatus::Cached); } @@ -232,42 +249,44 @@ pub async fn run_target( // attempt the process again in case it passes. let attempt_count = task.options.retry_count + 1; let mut attempt = 1; + let stream_output = is_primary || is_ci() && env::var("MOON_TEST").is_err(); let output; loop { - let possible_output; let attempt_comment = if attempt == 1 { String::new() } else { format!("(attempt {} of {})", attempt, attempt_count) }; - if is_primary { + let possible_output = if stream_output { // Print label *before* output is streamed since it may stay open forever, - // or use ANSI escape codes to alter the terminal. + // or it may use ANSI escape codes to alter the terminal. print_target_label(target_id, &attempt_comment, false); // If this target matches the primary target (the last task to run), // then we want to stream the output directly to the parent (inherit mode). - possible_output = spawn_command(&mut command).await; + command.exec_stream_and_capture_output().await } else { // Otherwise we run the process in the background and write the output // once it has completed. - possible_output = exec_command(&mut command).await; - - // Print label *after* output has been captured, so parallel tasks - // aren't intertwined and the labels align with the output. - print_target_label(target_id, &attempt_comment, possible_output.is_err()); + command.exec_capture_output().await }; match possible_output { - Ok(o) => { - output = o; - break; - } - Err(e) => { - if attempt >= attempt_count { - return Err(WorkspaceError::Moon(e)); + // zero and non-zero exit codes + Ok(out) => { + if stream_output { + handle_streamed_output(target_id, &attempt_comment, &out); + } else { + handle_captured_output(target_id, &attempt_comment, &out); + } + + if out.status.success() { + output = out; + break; + } else if attempt >= attempt_count { + return Err(WorkspaceError::Moon(command.output_to_error(&out, false))); } else { attempt += 1; @@ -279,6 +298,10 @@ pub async fn run_target( ); } } + // process itself failed + Err(error) => { + return Err(WorkspaceError::Moon(error)); + } } } @@ -292,11 +315,6 @@ pub async fn run_target( .await?; } - // Delete the old hash - if !cache.item.hash.is_empty() && cache.item.hash != hash { - workspace.cache.delete_hash(&cache.item.hash).await?; - } - // Save the new hash workspace.cache.save_hash(&hash, &hasher).await?; @@ -308,36 +326,65 @@ pub async fn run_target( cache.item.stdout = output_to_string(&output.stdout); cache.save().await?; - print_cache_item(&cache.item, !is_primary); - Ok(ActionStatus::Passed) } fn print_target_label(target: &str, comment: &str, failed: bool) { - let label = if failed { + let mut label = if failed { label_run_target_failed(target) } else { label_run_target(target) }; - if comment.is_empty() { - println!("{}", label); + if !comment.is_empty() { + label = format!("{} {}", label, color::muted(comment)); + }; + + if failed { + eprintln!("{}", label); } else { - println!("{} {}", label, color::muted(comment)); + println!("{}", label); } } -fn print_cache_item(item: &RunTargetState, log: bool) { - // Only log when *not* the primary target, or a cache hit - if log { - if !item.stderr.is_empty() { - eprintln!("{}", item.stderr.trim()); - eprintln!(); - } +fn print_cache_item(item: &RunTargetState) { + if !item.stderr.is_empty() { + eprintln!("{}", item.stderr.trim()); + eprintln!(); + } - if !item.stdout.is_empty() { - println!("{}", item.stdout.trim()); - println!(); - } + if !item.stdout.is_empty() { + println!("{}", item.stdout.trim()); + println!(); + } +} + +fn print_output_std(output: &Output) { + let stderr = output_to_string(&output.stderr); + let stdout = output_to_string(&output.stdout); + + if !stderr.is_empty() { + eprintln!("{}", stderr.trim()); + eprintln!(); + } + + if !stdout.is_empty() { + println!("{}", stdout.trim()); + println!(); + } +} + +// Print label *after* output has been captured, so parallel tasks +// aren't intertwined and the labels align with the output. +fn handle_captured_output(target_id: &str, attempt_comment: &str, output: &Output) { + print_target_label(target_id, attempt_comment, !output.status.success()); + print_output_std(output); +} + +// Only print the label when the process has failed, +// as the actual output has already been streamed to the console. +fn handle_streamed_output(target_id: &str, attempt_comment: &str, output: &Output) { + if !output.status.success() { + print_target_label(target_id, attempt_comment, true); } } diff --git a/crates/workspace/src/actions/setup_toolchain.rs b/crates/workspace/src/actions/setup_toolchain.rs index d40de05f0a7..39def09913d 100644 --- a/crates/workspace/src/actions/setup_toolchain.rs +++ b/crates/workspace/src/actions/setup_toolchain.rs @@ -17,9 +17,8 @@ pub async fn setup_toolchain( "Setting up toolchain", ); - let workspace = workspace.read().await; + let mut workspace = workspace.write().await; let mut cache = workspace.cache.cache_workspace_state().await?; - let mut root_package = workspace.load_package_json().await?; // Only check the versions of some tools every 12 hours, // as checking every run has considerable overhead spawning all @@ -28,10 +27,8 @@ pub async fn setup_toolchain( let check_versions = cache.item.last_version_check_time == 0 || (cache.item.last_version_check_time + HOUR * 12) <= now; - let installed_tools = workspace - .toolchain - .setup(&mut root_package, check_versions) - .await?; + // Install all tools + let installed_tools = workspace.toolchain.setup(check_versions).await?; // Update the cache with the timestamp if check_versions { @@ -39,7 +36,7 @@ pub async fn setup_toolchain( cache.save().await?; } - Ok(if installed_tools { + Ok(if installed_tools > 0 { ActionStatus::Passed } else { ActionStatus::Skipped diff --git a/crates/workspace/src/actions/sync_project.rs b/crates/workspace/src/actions/sync_project.rs index 26430b42c4d..9fe1fbc7dc4 100644 --- a/crates/workspace/src/actions/sync_project.rs +++ b/crates/workspace/src/actions/sync_project.rs @@ -1,7 +1,9 @@ use crate::action::ActionStatus; use crate::errors::WorkspaceError; use crate::workspace::Workspace; +use moon_config::{tsconfig::TsConfigJson, TypeScriptConfig}; use moon_logger::{color, debug}; +use moon_project::Project; use moon_utils::is_ci; use pathdiff::diff_paths; use std::path::PathBuf; @@ -10,88 +12,128 @@ use tokio::sync::RwLock; const TARGET: &str = "moon:action:sync-project"; +fn sync_root_tsconfig( + tsconfig: &mut TsConfigJson, + typescript_config: &TypeScriptConfig, + project: &Project, +) -> bool { + if project + .root + .join(&typescript_config.project_config_file_name) + .exists() + && tsconfig.add_project_ref(&project.source, &typescript_config.project_config_file_name) + { + debug!( + target: TARGET, + "Syncing {} as a project reference to the root {}", + color::id(&project.id), + color::file(&typescript_config.root_config_file_name) + ); + + return true; + } + + false +} + pub async fn sync_project( workspace: Arc>, project_id: &str, ) -> Result { - let workspace = workspace.read().await; - let project = workspace.projects.load(project_id)?; let mut mutated_files = false; + let mut typescript_config; + + // Read only + { + let workspace = workspace.read().await; + let project = workspace.projects.load(project_id)?; + let node_config = &workspace.config.node; + + // Copy values outside of this block + typescript_config = workspace.config.typescript.clone(); + + // Sync each dependency to `tsconfig.json` and `package.json` + let package_manager = workspace.toolchain.get_node().get_package_manager(); + let mut project_package_json = project.load_package_json().await?; + let mut project_tsconfig_json = project + .load_tsconfig_json(&typescript_config.project_config_file_name) + .await?; + + for dep_id in project.get_dependencies() { + let dep_project = workspace.projects.load(&dep_id)?; - // Sync a project reference to the root `tsconfig.json` - let node_config = &workspace.config.node; - let typescript_config = &workspace.config.typescript; - let tsconfig_root_name = &typescript_config.root_config_file_name; - let tsconfig_branch_name = &typescript_config.project_config_file_name; - - if typescript_config.sync_project_references { - if let Some(mut tsconfig) = workspace.load_tsconfig_json(tsconfig_root_name).await? { - if tsconfig.add_project_ref(&project.source, tsconfig_branch_name) { - debug!( - target: TARGET, - "Syncing {} as a project reference to the root {}", - color::id(project_id), - color::file(tsconfig_root_name) - ); - - tsconfig.save().await?; - mutated_files = true; + // Update `dependencies` within this project's `package.json` + if node_config.sync_project_workspace_dependencies { + if let Some(package_json) = &mut project_package_json { + let dep_package_name = + dep_project.get_package_name().await?.unwrap_or_default(); + + // Only add if the dependent project has a `package.json`, + // and this `package.json` has not already declared the dep. + if !dep_package_name.is_empty() + && package_json.add_dependency( + dep_package_name, + package_manager.get_workspace_dependency_range(), + true, + ) + { + debug!( + target: TARGET, + "Syncing {} as a dependency to {}'s {}", + color::id(&dep_id), + color::id(project_id), + color::file("package.json") + ); + + package_json.save().await?; + mutated_files = true; + } + } } - } - } - // Sync each dependency to `tsconfig.json` and `package.json` - let manager = workspace.toolchain.get_node_package_manager(); - - for dep_id in project.get_dependencies() { - let dep_project = workspace.projects.load(&dep_id)?; - - // Update `dependencies` within `tsconfig.json` - if node_config.sync_project_workspace_dependencies { - if let Some(mut package) = project.load_package_json().await? { - let dep_package_name = dep_project.get_package_name().await?.unwrap_or_default(); - - // Only add if the dependent project has a `package.json`, - // and this `package.json` has not already declared the dep. - if !dep_package_name.is_empty() - && package.add_dependency( - dep_package_name, - manager.get_workspace_dependency_range(), - true, - ) - { - debug!( - target: TARGET, - "Syncing {} as a dependency to {}'s {}", - color::id(&dep_id), - color::id(project_id), - color::file("package.json") + // Update `references` within this project's `tsconfig.json` + if typescript_config.sync_project_references { + if let Some(tsconfig_json) = &mut project_tsconfig_json { + let tsconfig_branch_name = &typescript_config.project_config_file_name; + let dep_ref_path = String::from( + diff_paths(&dep_project.root, &project.root) + .unwrap_or_else(|| PathBuf::from(".")) + .to_string_lossy(), ); - package.save().await?; - mutated_files = true; + // Only add if the dependent project has a `tsconfig.json`, + // and this `tsconfig.json` has not already declared the dep. + if dep_project.root.join(tsconfig_branch_name).exists() + && tsconfig_json.add_project_ref(&dep_ref_path, tsconfig_branch_name) + { + debug!( + target: TARGET, + "Syncing {} as a project reference to {}'s {}", + color::id(&dep_id), + color::id(project_id), + color::file(tsconfig_branch_name) + ); + + tsconfig_json.save().await?; + mutated_files = true; + } + } else { + // Projects doesnt have a `tsconfig.json` + typescript_config.sync_project_references = false; } } } + } - // Update `references` within `tsconfig.json` + // Writes root `tsconfig.json` + { + // Sync a project reference if typescript_config.sync_project_references { - if let Some(mut tsconfig) = project.load_tsconfig_json(tsconfig_branch_name).await? { - let dep_ref_path = String::from( - diff_paths(&project.root, &dep_project.root) - .unwrap_or_else(|| PathBuf::from(".")) - .to_string_lossy(), - ); - - if tsconfig.add_project_ref(&dep_ref_path, tsconfig_branch_name) { - debug!( - target: TARGET, - "Syncing {} as a project reference to {}'s {}", - color::id(&dep_id), - color::id(project_id), - color::file(tsconfig_branch_name) - ); + let mut workspace = workspace.write().await; + let project = workspace.projects.load(project_id)?; + if let Some(tsconfig) = &mut workspace.tsconfig_json { + if sync_root_tsconfig(tsconfig, &typescript_config, &project) { tsconfig.save().await?; mutated_files = true; } diff --git a/crates/workspace/src/dep_graph.rs b/crates/workspace/src/dep_graph.rs index 453a5051a77..c9a383db258 100644 --- a/crates/workspace/src/dep_graph.rs +++ b/crates/workspace/src/dep_graph.rs @@ -1,6 +1,8 @@ use crate::errors::WorkspaceError; use moon_logger::{color, debug, trace}; -use moon_project::{ProjectGraph, Target, TargetError, TargetProject, TouchedFilePaths}; +use moon_project::{ + ProjectGraph, ProjectID, Target, TargetError, TargetID, TargetProject, TouchedFilePaths, +}; use petgraph::algo::toposort; use petgraph::dot::{Config, Dot}; use petgraph::graph::DiGraph; @@ -13,9 +15,9 @@ const TARGET: &str = "moon:dep-graph"; pub enum Node { InstallNodeDeps, - RunTarget(String), // target id + RunTarget(TargetID), SetupToolchain, - SyncProject(String), // project id + SyncProject(ProjectID), } impl Node { @@ -264,7 +266,6 @@ impl DepGraph { fn detect_cycle(&self) -> Result<(), WorkspaceError> { use petgraph::algo::kosaraju_scc; - // TODO: Not exactly accurate, revisit!!! let scc = kosaraju_scc(&self.graph); let cycle = scc .last() @@ -272,7 +273,7 @@ impl DepGraph { .iter() .map(|i| self.get_node_from_index(*i).unwrap().label()) .collect::>() - .join(" -> "); + .join(" → "); Err(WorkspaceError::DepGraphCycleDetected(cycle)) } @@ -334,11 +335,11 @@ impl DepGraph { ); // We should sync projects *before* running targets - let project_node = self.sync_project(&project.id, projects)?; + let sync_project_index = self.sync_project(&project.id, projects)?; let node = self.graph.add_node(Node::RunTarget(target_id.to_owned())); self.graph.add_edge(node, self.install_node_deps_index, ()); - self.graph.add_edge(node, project_node, ()); + self.graph.add_edge(node, sync_project_index, ()); // Also cache so we don't run the same target multiple times self.index_cache.insert(target_id.to_owned(), node); @@ -462,7 +463,7 @@ mod tests { #[test] #[should_panic( - expected = "CycleDetected(\"RunTarget(cycle:a) -> RunTarget(cycle:b) -> RunTarget(cycle:c)\")" + expected = "CycleDetected(\"RunTarget(cycle:a) → RunTarget(cycle:b) → RunTarget(cycle:c)\")" )] fn detects_cycles() { let projects = create_tasks_project_graph(); diff --git a/crates/workspace/src/errors.rs b/crates/workspace/src/errors.rs index 0673de7b754..fd79f9038f9 100644 --- a/crates/workspace/src/errors.rs +++ b/crates/workspace/src/errors.rs @@ -12,7 +12,7 @@ pub enum WorkspaceError { #[error("Unknown node {0} found in dependency graph. How did this get here?")] DepGraphUnknownNode(usize), - #[error("Action runner failed to run: {0}")] + #[error("{0}")] ActionRunnerFailure(String), #[error( @@ -34,13 +34,6 @@ pub enum WorkspaceError { )] MissingWorkspaceConfigFile, - #[error( - "Unable to locate {}/{} configuration file.", - constants::CONFIG_DIRNAME, - constants::CONFIG_PROJECT_FILENAME - )] - MissingGlobalProjectConfigFile, - #[error( "Failed to validate {}/{} configuration file.\n\n{0}", constants::CONFIG_DIRNAME, diff --git a/crates/workspace/src/vcs/git.rs b/crates/workspace/src/vcs/git.rs index a0e32d17736..74ce689c99f 100644 --- a/crates/workspace/src/vcs/git.rs +++ b/crates/workspace/src/vcs/git.rs @@ -1,6 +1,7 @@ use crate::vcs::{TouchedFiles, Vcs, VcsResult}; use async_trait::async_trait; -use moon_utils::process::{create_command, exec_command_capture_stdout}; +use moon_utils::fs; +use moon_utils::process::{output_to_string, output_to_trimmed_string, Command}; use regex::Regex; use std::collections::{BTreeMap, HashSet}; use std::path::{Path, PathBuf}; @@ -17,17 +18,37 @@ impl Git { working_dir: working_dir.to_path_buf(), } } + + async fn run_command(&self, command: &mut Command, trim: bool) -> VcsResult { + let output = command.exec_capture_output().await?; + + if trim { + return Ok(output_to_trimmed_string(&output.stdout)); + } + + Ok(output_to_string(&output.stdout)) + } } #[async_trait] impl Vcs for Git { + fn create_command(&self, args: Vec<&str>) -> Command { + let mut cmd = Command::new("git"); + cmd.args(args).cwd(&self.working_dir); + cmd + } + async fn get_local_branch(&self) -> VcsResult { - self.run_command(vec!["branch", "--show-current"], true) - .await + self.run_command( + &mut self.create_command(vec!["branch", "--show-current"]), + true, + ) + .await } async fn get_local_branch_revision(&self) -> VcsResult { - self.run_command(vec!["rev-parse", "HEAD"], true).await + self.run_command(&mut self.create_command(vec!["rev-parse", "HEAD"]), true) + .await } fn get_default_branch(&self) -> &str { @@ -35,18 +56,20 @@ impl Vcs for Git { } async fn get_default_branch_revision(&self) -> VcsResult { - self.run_command(vec!["rev-parse", &self.default_branch], true) - .await + self.run_command( + &mut self.create_command(vec!["rev-parse", &self.default_branch]), + true, + ) + .await } async fn get_file_hashes(&self, files: &[String]) -> VcsResult> { - let mut args = vec!["hash-object"]; - - for file in files { - args.push(file); - } + let output = self + .create_command(vec!["hash-object", "--stdin-paths"]) + .exec_capture_output_with_input(&files.join("\n")) + .await?; + let output = output_to_trimmed_string(&output.stdout); - let output = self.run_command(args, true).await?; let mut map = BTreeMap::new(); for (index, hash) in output.split('\n').enumerate() { @@ -60,7 +83,10 @@ impl Vcs for Git { async fn get_file_tree_hashes(&self, dir: &str) -> VcsResult> { let output = self - .run_command(vec!["ls-tree", "HEAD", "-r", dir], true) + .run_command( + &mut self.create_command(vec!["ls-tree", "HEAD", "-r", dir]), + true, + ) .await?; let mut map = BTreeMap::new(); @@ -82,14 +108,14 @@ impl Vcs for Git { async fn get_touched_files(&self) -> VcsResult { let output = self .run_command( - vec![ + &mut self.create_command(vec![ "status", "--porcelain", "--untracked-files", // We use this option so that file names with special characters // are displayed as-is and are not quoted/escaped "-z", - ], + ]), false, ) .await?; @@ -197,7 +223,7 @@ impl Vcs for Git { ) -> VcsResult { let output = self .run_command( - vec![ + &mut self.create_command(vec![ "--no-pager", "diff", "--name-status", @@ -208,7 +234,7 @@ impl Vcs for Git { "-z", base_revision, revision, - ], + ]), false, ) .await?; @@ -287,18 +313,7 @@ impl Vcs for Git { false } - async fn run_command(&self, args: Vec<&str>, trim: bool) -> VcsResult { - let output = exec_command_capture_stdout( - create_command("git") - .args(args) - .current_dir(&self.working_dir), - ) - .await?; - - if trim { - return Ok(output.trim().to_owned()); - } - - Ok(output) + fn is_enabled(&self) -> bool { + fs::find_upwards(".git", &self.working_dir).is_some() } } diff --git a/crates/workspace/src/vcs/mod.rs b/crates/workspace/src/vcs/mod.rs index 85d2ce1e85c..91555fd753b 100644 --- a/crates/workspace/src/vcs/mod.rs +++ b/crates/workspace/src/vcs/mod.rs @@ -5,6 +5,7 @@ use crate::errors::WorkspaceError; use async_trait::async_trait; use git::Git; use moon_config::{VcsManager as VM, WorkspaceConfig}; +use moon_utils::process::Command; use std::collections::{BTreeMap, HashSet}; use std::path::Path; use svn::Svn; @@ -27,6 +28,9 @@ pub struct TouchedFiles { #[async_trait] pub trait Vcs { + /// Create a process command for the underlying vcs binary. + fn create_command(&self, args: Vec<&str>) -> Command; + /// Get the local checkout branch name. async fn get_local_branch(&self) -> VcsResult; @@ -66,8 +70,8 @@ pub trait Vcs { /// Return true if the provided branch matches the default branch. fn is_default_branch(&self, branch: &str) -> bool; - /// Execute the underlying vcs binary. - async fn run_command(&self, args: Vec<&str>, trim: bool) -> VcsResult; + /// Return true if the repo is currently VCS enabled. + fn is_enabled(&self) -> bool; } pub struct VcsManager {} diff --git a/crates/workspace/src/vcs/svn.rs b/crates/workspace/src/vcs/svn.rs index 91d2d8eba44..71527cb56ab 100644 --- a/crates/workspace/src/vcs/svn.rs +++ b/crates/workspace/src/vcs/svn.rs @@ -1,9 +1,12 @@ use crate::vcs::{TouchedFiles, Vcs, VcsResult}; use async_trait::async_trait; -use moon_utils::process::{create_command, exec_command_capture_stdout}; +use moon_utils::fs; +use moon_utils::process::{output_to_string, output_to_trimmed_string, Command}; use regex::Regex; use std::collections::{BTreeMap, HashSet}; +use std::fs::metadata; use std::path::{Path, PathBuf}; +use std::time::SystemTime; // TODO: This code hasn't been tested yet and may not be accurate! @@ -31,7 +34,9 @@ impl Svn { } async fn get_revision_number(&self, revision: &str) -> VcsResult { - let output = self.run_command(vec!["info", "-r", revision], true).await?; + let output = self + .run_command(&mut self.create_command(vec!["info", "-r", revision]), true) + .await?; Ok(self.extract_line_from_info("Revision:", &output)) } @@ -96,13 +101,31 @@ impl Svn { untracked, } } + + async fn run_command(&self, command: &mut Command, trim: bool) -> VcsResult { + let output = command.exec_capture_output().await?; + + if trim { + return Ok(output_to_trimmed_string(&output.stdout)); + } + + Ok(output_to_string(&output.stdout)) + } } // https://edoras.sdsu.edu/doc/svn-book-html-chunk/svn.ref.svn.c.info.html #[async_trait] impl Vcs for Svn { + fn create_command(&self, args: Vec<&str>) -> Command { + let mut cmd = Command::new("svn"); + cmd.args(args).cwd(&self.working_dir); + cmd + } + async fn get_local_branch(&self) -> VcsResult { - let output = self.run_command(vec!["info"], false).await?; + let output = self + .run_command(&mut self.create_command(vec!["info"]), false) + .await?; let url = self.extract_line_from_info("URL:", &output); let pattern = Regex::new("branches/([^/]+)").unwrap(); @@ -134,9 +157,20 @@ impl Vcs for Svn { let mut map = BTreeMap::new(); // svn doesnt support file hashing, so instead of generating some - // random hash ourselves, just pass an emptry string. + // random hash ourselves, just use the modified time. for file in files { - map.insert(file.to_owned(), String::new()); + if let Ok(metadata) = metadata(file) { + if let Ok(modified) = metadata.modified() { + map.insert( + file.to_owned(), + modified + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() + .to_string(), + ); + } + } } Ok(map) @@ -146,7 +180,10 @@ impl Vcs for Svn { let mut map = BTreeMap::new(); let output = self - .run_command(vec!["ls", "--recursive", "--depth", "infinity", dir], false) + .run_command( + &mut self.create_command(vec!["ls", "--recursive", "--depth", "infinity", dir]), + false, + ) .await?; // svn doesnt support file hashing, so instead of generating some @@ -160,7 +197,9 @@ impl Vcs for Svn { // https://svnbook.red-bean.com/en/1.8/svn.ref.svn.c.status.html async fn get_touched_files(&self) -> VcsResult { - let output = self.run_command(vec!["status", "wc"], false).await?; + let output = self + .run_command(&mut self.create_command(vec!["status", "wc"]), false) + .await?; Ok(Svn::process_touched_files(output)) } @@ -185,12 +224,12 @@ impl Vcs for Svn { ) -> VcsResult { let output = self .run_command( - vec![ + &mut self.create_command(vec![ "diff", "-r", &format!("{}:{}", base_revision, revision), "--summarize", - ], + ]), false, ) .await?; @@ -202,18 +241,7 @@ impl Vcs for Svn { self.default_branch == branch } - async fn run_command(&self, args: Vec<&str>, trim: bool) -> VcsResult { - let output = exec_command_capture_stdout( - create_command("svn") - .args(args) - .current_dir(&self.working_dir), - ) - .await?; - - if trim { - return Ok(output.trim().to_owned()); - } - - Ok(output) + fn is_enabled(&self) -> bool { + fs::find_upwards(".svn", &self.working_dir).is_some() } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0eb7dfa13a7..bfde8c5fc2c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7,30 +7,21 @@ use moon_config::{constants, GlobalProjectConfig, WorkspaceConfig}; use moon_logger::{color, debug, trace}; use moon_project::ProjectGraph; use moon_toolchain::Toolchain; +use moon_utils::fs; use std::env; use std::path::{Path, PathBuf}; /// Recursively attempt to find the workspace root by locating the ".moon" /// configuration folder, starting from the current working directory. fn find_workspace_root(current_dir: PathBuf) -> Option { - let config_dir = current_dir.join(constants::CONFIG_DIRNAME); - trace!( target: "moon:workspace", "Attempting to find workspace root at {}", color::path(¤t_dir), ); - if config_dir.exists() { - return Some(current_dir); - } - - let parent_dir = current_dir.parent(); - - match parent_dir { - Some(dir) => find_workspace_root(dir.to_path_buf()), - None => None, - } + fs::find_upwards(constants::CONFIG_DIRNAME, ¤t_dir) + .map(|dir| dir.parent().unwrap().to_path_buf()) } // project.yml @@ -52,7 +43,7 @@ fn load_global_project_config(root_dir: &Path) -> Result Result Result { + let package_json_path = root_dir.join("package.json"); + + trace!( + target: "moon:workspace", + "Attempting to find {} in {}", + color::file("package.json"), + color::path(root_dir), + ); + + if !package_json_path.exists() { + return Err(WorkspaceError::MissingPackageJson); + } + + Ok(PackageJson::load(&package_json_path).await?) +} + +// tsconfig.json +async fn load_tsconfig_json( + root_dir: &Path, + tsconfig_name: &str, +) -> Result, WorkspaceError> { + let tsconfig_json_path = root_dir.join(tsconfig_name); + + trace!( + target: "moon:workspace", + "Attempting to find {} in {}", + color::file(tsconfig_name), + color::path(root_dir), + ); + + if !tsconfig_json_path.exists() { + return Ok(None); + } + + Ok(Some(TsConfigJson::load(&tsconfig_json_path).await?)) +} + pub struct Workspace { /// Engine for reading and writing cache/outputs. pub cache: CacheEngine, @@ -96,6 +126,9 @@ pub struct Workspace { /// Workspace configuration loaded from ".moon/workspace.yml". pub config: WorkspaceConfig, + /// The root `package.json`. + pub package_json: PackageJson, + /// The project graph, where each project is lazy loaded in. pub projects: ProjectGraph, @@ -105,6 +138,9 @@ pub struct Workspace { /// The toolchain instance that houses all runtime tools/languages. pub toolchain: Toolchain, + /// The root `tsconfig.json`. + pub tsconfig_json: Option, + /// The current working directory. pub working_dir: PathBuf, } @@ -129,6 +165,9 @@ impl Workspace { // Load configs let config = load_workspace_config(&root_dir)?; let project_config = load_global_project_config(&root_dir)?; + let package_json = load_package_json(&root_dir).await?; + let tsconfig_json = + load_tsconfig_json(&root_dir, &config.typescript.root_config_file_name).await?; // Setup components let cache = CacheEngine::create(&root_dir).await?; @@ -138,9 +177,11 @@ impl Workspace { Ok(Workspace { cache, config, + package_json, projects, root: root_dir, toolchain, + tsconfig_json, working_dir, }) } @@ -149,43 +190,4 @@ impl Workspace { pub fn detect_vcs(&self) -> Box { VcsManager::load(&self.config, &self.working_dir) } - - /// Load and parse the root `package.json`. - pub async fn load_package_json(&self) -> Result { - let package_json_path = self.root.join("package.json"); - - trace!( - target: "moon:workspace", - "Attempting to find {} in {}", - color::file("package.json"), - color::path(&self.root), - ); - - if !package_json_path.exists() { - return Err(WorkspaceError::MissingPackageJson); - } - - Ok(PackageJson::load(&package_json_path).await?) - } - - /// Load and parse the root `tsconfig.json` if it exists. - pub async fn load_tsconfig_json( - &self, - tsconfig_name: &str, - ) -> Result, WorkspaceError> { - let tsconfig_json_path = self.root.join(tsconfig_name); - - trace!( - target: "moon:workspace", - "Attempting to find {} in {}", - color::file(tsconfig_name), - color::path(&self.root), - ); - - if !tsconfig_json_path.exists() { - return Ok(None); - } - - Ok(Some(TsConfigJson::load(&tsconfig_json_path).await?)) - } } diff --git a/docs/roadmap.md b/docs/roadmap.md index 9719d266bb5..4a1dad807d1 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -62,7 +62,8 @@ - [x] Installs npm dependencies - [x] Syncs `package.json` and `tsconfig.json` for all projects - [x] Writes JSON preserving field order -- [ ] Handle non-0 exit codes +- [x] Handle non-0 exit codes +- [x] Handle offline ## CLI @@ -75,6 +76,7 @@ - [x] `run` command to run targets - [x] Args after `--` are passed to the underlying command - [x] Only run on affected changes + - [x] Run multiple targets - [x] `ci` command for smart running affected targets (below) ## CI @@ -87,11 +89,11 @@ - [x] add a `--no-cache` option to disable all caching - [ ] hashing - - [ ] use `stdin` for commands that take long arguments - - [ ] dont load `package.json`/`tsconfig.json` so much + - [x] use `stdin` for commands that take long arguments + - [x] dont load `package.json`/`tsconfig.json` so much - [x] delete old hashes when the hash changes - [ ] ignore hashes for files that are gitignored - - [ ] include local file changes in hash + - [x] include local file changes in hash # 0.2.0 @@ -107,10 +109,10 @@ ## Action runner - [ ] Add a debug layer so that the node processes can be inspected +- [ ] Write output logs for every action ## CLI -- [ ] `run` - - [ ] All projects for target (`*`) +- [ ] `run-many` - [ ] `graph` - [ ] Spin up an interactive website with full project/task data diff --git a/docs/workspace.md b/docs/workspace.md index 9569adc7ee6..feed81719a7 100644 --- a/docs/workspace.md +++ b/docs/workspace.md @@ -198,7 +198,7 @@ Would result in the following `dependencies` within a project's `package.json`. ##### syncVersionManagerConfig The `syncVersionManagerConfig` setting syncs the currently configured [Node.js version](#version) to -a 3rd-party version manager's config/rc file. Supports `nodeenv` (syncs to `.node-version`), `nvm` +a 3rd-party version manager's config/rc file. Supports `nodenv` (syncs to `.node-version`), `nvm` (syncs to `.nvmrc`), or none (default). ```yaml diff --git a/packages/runtime/src/context.ts b/packages/runtime/src/context.ts index 633c01429e0..f1111585b80 100644 --- a/packages/runtime/src/context.ts +++ b/packages/runtime/src/context.ts @@ -10,7 +10,7 @@ export interface RuntimeContext { }; } -console.log('asdsasdssdsdsads'); +console.log('asdsaasdsdssdssdsdsads'); export async function getContext(): Promise { const { env } = process; diff --git a/schemas/workspace.json b/schemas/workspace.json index 9694fd5e929..dd3a51017d3 100644 --- a/schemas/workspace.json +++ b/schemas/workspace.json @@ -197,7 +197,7 @@ "VersionManager": { "type": "string", "enum": [ - "nodeenv", + "nodenv", "nvm" ] }, @@ -211,4 +211,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/fixtures/base/.moon/workspace.yml b/tests/fixtures/base/.moon/workspace.yml index 2114e85a056..511e2d347c7 100644 --- a/tests/fixtures/base/.moon/workspace.yml +++ b/tests/fixtures/base/.moon/workspace.yml @@ -1,4 +1,4 @@ node: - # We use an old version to avoid conflicts within local dev - version: '10.0.0' + version: '16.0.0' + projects: {} diff --git a/tests/fixtures/base/package.json b/tests/fixtures/base/package.json index 0967ef424bc..de96ce4fe86 100644 --- a/tests/fixtures/base/package.json +++ b/tests/fixtures/base/package.json @@ -1 +1,4 @@ -{} +{ + "name": "test-base", + "private": true +} diff --git a/tests/fixtures/cases/.moon/workspace.yml b/tests/fixtures/cases/.moon/workspace.yml new file mode 100644 index 00000000000..3946aaa06a8 --- /dev/null +++ b/tests/fixtures/cases/.moon/workspace.yml @@ -0,0 +1,28 @@ +projects: + base: 'base' + + # Project/task deps + depsA: 'deps-a' + depsB: 'deps-b' + depsC: 'deps-c' + dependsOn: 'depends-on' + + # Target scopes + targetScopeA: 'target-scope-a' + targetScopeB: 'target-scope-b' + targetScopeC: 'target-scope-c' + + # Tool types + node: 'node' + system: 'system' + systemWindows: 'system-windows' + +typescript: + syncProjectReferences: false + +# Put at the bottom so we can append settings to test +node: + version: '16.0.0' + addEnginesConstraint: false + dedupeOnLockfileChange: false + syncProjectWorkspaceDependencies: false diff --git a/tests/fixtures/cases/base/project.yml b/tests/fixtures/cases/base/project.yml new file mode 100644 index 00000000000..ada2df5a855 --- /dev/null +++ b/tests/fixtures/cases/base/project.yml @@ -0,0 +1 @@ +tasks: {} diff --git a/tests/fixtures/cases/depends-on/package.json b/tests/fixtures/cases/depends-on/package.json new file mode 100644 index 00000000000..7126ec89920 --- /dev/null +++ b/tests/fixtures/cases/depends-on/package.json @@ -0,0 +1,6 @@ +{ + "name": "test-cases-depends-on", + "dependencies": { + "react": "17.0.0" + } +} diff --git a/tests/fixtures/cases/depends-on/project.yml b/tests/fixtures/cases/depends-on/project.yml new file mode 100644 index 00000000000..2d87ff06347 --- /dev/null +++ b/tests/fixtures/cases/depends-on/project.yml @@ -0,0 +1,9 @@ +dependsOn: + - depsA + - depsB + - depsC + +tasks: + standard: + command: node + args: -e "'noop'" diff --git a/tests/fixtures/cases/depends-on/tsconfig.json b/tests/fixtures/cases/depends-on/tsconfig.json new file mode 100644 index 00000000000..a0e4939755f --- /dev/null +++ b/tests/fixtures/cases/depends-on/tsconfig.json @@ -0,0 +1,3 @@ +{ + "exclude": ["*.js"] +} diff --git a/tests/fixtures/cases/deps-a/package.json b/tests/fixtures/cases/deps-a/package.json new file mode 100644 index 00000000000..6448d9946f8 --- /dev/null +++ b/tests/fixtures/cases/deps-a/package.json @@ -0,0 +1,3 @@ +{ + "name": "test-cases-deps-a" +} diff --git a/tests/fixtures/cases/deps-a/project.yml b/tests/fixtures/cases/deps-a/project.yml new file mode 100644 index 00000000000..8ea01ab9f3d --- /dev/null +++ b/tests/fixtures/cases/deps-a/project.yml @@ -0,0 +1,16 @@ +tasks: + standard: + command: node + args: -e "console.log('deps=a')" + + dependencyOrder: + command: node + args: -e "console.log('deps=a')" + deps: + - 'depsB:dependencyOrder' + + # Cycle detection + taskCycle: + command: unknown + deps: + - 'depsB:taskCycle' diff --git a/tests/fixtures/cases/deps-b/package.json b/tests/fixtures/cases/deps-b/package.json new file mode 100644 index 00000000000..a8d42da0045 --- /dev/null +++ b/tests/fixtures/cases/deps-b/package.json @@ -0,0 +1,3 @@ +{ + "name": "test-cases-deps-b" +} diff --git a/tests/fixtures/cases/deps-b/project.yml b/tests/fixtures/cases/deps-b/project.yml new file mode 100644 index 00000000000..314c2352542 --- /dev/null +++ b/tests/fixtures/cases/deps-b/project.yml @@ -0,0 +1,16 @@ +tasks: + standard: + command: node + args: -e "console.log('deps=b')" + + dependencyOrder: + command: node + args: -e "console.log('deps=b')" + deps: + - 'depsC:dependencyOrder' + + # Cycle detection + taskCycle: + command: unknown + deps: + - 'depsC:taskCycle' diff --git a/tests/fixtures/cases/deps-b/tsconfig.json b/tests/fixtures/cases/deps-b/tsconfig.json new file mode 100644 index 00000000000..1a45a3f810e --- /dev/null +++ b/tests/fixtures/cases/deps-b/tsconfig.json @@ -0,0 +1,3 @@ +{ + "compilerOptions": {} +} diff --git a/tests/fixtures/cases/deps-c/project.yml b/tests/fixtures/cases/deps-c/project.yml new file mode 100644 index 00000000000..f12880d036a --- /dev/null +++ b/tests/fixtures/cases/deps-c/project.yml @@ -0,0 +1,14 @@ +tasks: + standard: + command: node + args: -e "console.log('deps=c')" + + dependencyOrder: + command: node + args: -e "console.log('deps=c')" + + # Cycle detection + taskCycle: + command: unknown + deps: + - 'depsA:taskCycle' diff --git a/tests/fixtures/cases/deps-c/tsconfig.json b/tests/fixtures/cases/deps-c/tsconfig.json new file mode 100644 index 00000000000..bf773db5f84 --- /dev/null +++ b/tests/fixtures/cases/deps-c/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["**/*"] +} diff --git a/tests/fixtures/cases/node/cjsFile.cjs b/tests/fixtures/cases/node/cjsFile.cjs new file mode 100644 index 00000000000..91382f66725 --- /dev/null +++ b/tests/fixtures/cases/node/cjsFile.cjs @@ -0,0 +1 @@ +require('./standard'); diff --git a/tests/fixtures/cases/node/cwd.js b/tests/fixtures/cases/node/cwd.js new file mode 100644 index 00000000000..47d4aa44cd6 --- /dev/null +++ b/tests/fixtures/cases/node/cwd.js @@ -0,0 +1 @@ +console.log(process.cwd().replace(/\\/g, '/')); diff --git a/tests/fixtures/cases/node/envVars.js b/tests/fixtures/cases/node/envVars.js new file mode 100644 index 00000000000..44b0fda6bce --- /dev/null +++ b/tests/fixtures/cases/node/envVars.js @@ -0,0 +1,5 @@ +['MOON_FOO', 'MOON_BAR', 'MOON_BAZ'].forEach((key) => { + if (process.env[key]) { + console.log(`${key}=${process.env[key].replace(/\\/g, '/')}`); + } +}); diff --git a/tests/fixtures/cases/node/envVarsMoon.js b/tests/fixtures/cases/node/envVarsMoon.js new file mode 100644 index 00000000000..69357e53fdd --- /dev/null +++ b/tests/fixtures/cases/node/envVarsMoon.js @@ -0,0 +1,5 @@ +Object.entries(process.env).forEach(([key, value]) => { + if (key.startsWith('MOON_') && !key.startsWith('MOON_TEST')) { + console.log(`${key}=${value.replace(/\\/g, '/')}`); + } +}); diff --git a/tests/fixtures/cases/node/exitCodeNonZero.js b/tests/fixtures/cases/node/exitCodeNonZero.js new file mode 100644 index 00000000000..1cea9d1c8c3 --- /dev/null +++ b/tests/fixtures/cases/node/exitCodeNonZero.js @@ -0,0 +1,6 @@ +console.log('stdout'); +console.error('stderr'); + +process.exitCode = 1; + +console.log('This should appear!'); diff --git a/tests/fixtures/cases/node/exitCodeZero.js b/tests/fixtures/cases/node/exitCodeZero.js new file mode 100644 index 00000000000..887c40bee28 --- /dev/null +++ b/tests/fixtures/cases/node/exitCodeZero.js @@ -0,0 +1,6 @@ +console.log('stdout'); +console.error('stderr'); + +process.exitCode = 0; + +console.log('This should appear!'); diff --git a/tests/fixtures/cases/node/mjsFile.mjs b/tests/fixtures/cases/node/mjsFile.mjs new file mode 100644 index 00000000000..a5b6817de5c --- /dev/null +++ b/tests/fixtures/cases/node/mjsFile.mjs @@ -0,0 +1 @@ +import './standard.js'; diff --git a/tests/fixtures/cases/node/passthroughArgs.js b/tests/fixtures/cases/node/passthroughArgs.js new file mode 100644 index 00000000000..83cf03c37aa --- /dev/null +++ b/tests/fixtures/cases/node/passthroughArgs.js @@ -0,0 +1 @@ +console.log(process.argv.slice(2).join(' ')); diff --git a/tests/fixtures/cases/node/processExitNonZero.js b/tests/fixtures/cases/node/processExitNonZero.js new file mode 100644 index 00000000000..2fe41b280bb --- /dev/null +++ b/tests/fixtures/cases/node/processExitNonZero.js @@ -0,0 +1,6 @@ +console.log('stdout'); +console.error('stderr'); + +process.exit(1); + +console.log('This should not appear!'); diff --git a/tests/fixtures/cases/node/processExitZero.js b/tests/fixtures/cases/node/processExitZero.js new file mode 100644 index 00000000000..eb885b8c763 --- /dev/null +++ b/tests/fixtures/cases/node/processExitZero.js @@ -0,0 +1,6 @@ +console.log('stdout'); +console.error('stderr'); + +process.exit(0); + +console.log('This should not appear!'); diff --git a/tests/fixtures/cases/node/project.yml b/tests/fixtures/cases/node/project.yml new file mode 100644 index 00000000000..7b3a3626af1 --- /dev/null +++ b/tests/fixtures/cases/node/project.yml @@ -0,0 +1,60 @@ +tasks: + npm: + command: npm + args: config get tag + standard: + command: node + args: ./standard.js + cjs: + command: node + args: ./cjsFile.cjs + mjs: + command: node + args: ./mjsFile.mjs + exitCodeNonZero: + command: node + args: ./exitCodeNonZero.js + exitCodeZero: + command: node + args: ./exitCodeZero.js + processExitNonZero: + command: node + args: ./processExitNonZero.js + processExitZero: + command: node + args: ./processExitZero.js + throwError: + command: node + args: ./throwError.js + unhandledPromise: + command: node + args: ./unhandledPromise.js + topLevelAwait: + command: node + args: ./topLevelAwait.mjs + passthroughArgs: + command: node + args: ./passthroughArgs.js + envVars: + command: node + args: ./envVars.js + env: + MOON_FOO: abc + MOON_BAR: '123' + MOON_BAZ: 'true' + envVarsMoon: + command: node + args: ./envVarsMoon.js + runFromProject: + command: node + args: ./cwd.js + runFromWorkspace: + command: node + args: ./node/cwd.js + options: + runFromWorkspaceRoot: true + retryCount: + command: node + args: ./processExitNonZero.js + options: + retryCount: 3 diff --git a/tests/fixtures/cases/node/standard.js b/tests/fixtures/cases/node/standard.js new file mode 100644 index 00000000000..8d4f87187c2 --- /dev/null +++ b/tests/fixtures/cases/node/standard.js @@ -0,0 +1,2 @@ +console.log('stdout'); +console.error('stderr'); diff --git a/tests/fixtures/cases/node/throwError.js b/tests/fixtures/cases/node/throwError.js new file mode 100644 index 00000000000..9c5b8d2297f --- /dev/null +++ b/tests/fixtures/cases/node/throwError.js @@ -0,0 +1,4 @@ +console.log('stdout'); +console.error('stderr'); + +throw new Error('Oops'); diff --git a/tests/fixtures/cases/node/topLevelAwait.mjs b/tests/fixtures/cases/node/topLevelAwait.mjs new file mode 100644 index 00000000000..50945a9865e --- /dev/null +++ b/tests/fixtures/cases/node/topLevelAwait.mjs @@ -0,0 +1,10 @@ +console.log('before'); + +await new Promise((resolve) => { + setTimeout(() => { + console.log('awaiting'); + resolve(); + }, 100); +}); + +console.log('after'); diff --git a/tests/fixtures/cases/node/unhandledPromise.js b/tests/fixtures/cases/node/unhandledPromise.js new file mode 100644 index 00000000000..909d23eeb44 --- /dev/null +++ b/tests/fixtures/cases/node/unhandledPromise.js @@ -0,0 +1,6 @@ +console.log('stdout'); +console.error('stderr'); + +new Promise((resolve, reject) => { + reject('Oops'); +}); diff --git a/tests/fixtures/cases/package.json b/tests/fixtures/cases/package.json new file mode 100644 index 00000000000..21ae0b51024 --- /dev/null +++ b/tests/fixtures/cases/package.json @@ -0,0 +1,7 @@ +{ + "name": "test-cases", + "private": true, + "workspaces": [ + "*" + ] +} diff --git a/tests/fixtures/cases/system-windows/cwd.bat b/tests/fixtures/cases/system-windows/cwd.bat new file mode 100644 index 00000000000..f97d5bfb654 --- /dev/null +++ b/tests/fixtures/cases/system-windows/cwd.bat @@ -0,0 +1 @@ +echo %cd% \ No newline at end of file diff --git a/tests/fixtures/cases/system-windows/envVars.bat b/tests/fixtures/cases/system-windows/envVars.bat new file mode 100644 index 00000000000..af068672f8e --- /dev/null +++ b/tests/fixtures/cases/system-windows/envVars.bat @@ -0,0 +1,3 @@ +echo MOON_FOO=%MOON_FOO% +echo MOON_BAR=%MOON_BAR% +echo MOON_BAZ=%MOON_BAZ% diff --git a/tests/fixtures/cases/system-windows/envVarsMoon.bat b/tests/fixtures/cases/system-windows/envVarsMoon.bat new file mode 100644 index 00000000000..5985489fa66 --- /dev/null +++ b/tests/fixtures/cases/system-windows/envVarsMoon.bat @@ -0,0 +1,3 @@ +:: Looping over env vars isn't easy, so just check a few of them +echo MOON_RUN_TARGET=%MOON_RUN_TARGET% +echo MOON_PROJECT_ID=%MOON_PROJECT_ID% \ No newline at end of file diff --git a/tests/fixtures/cases/system-windows/exitNonZero.bat b/tests/fixtures/cases/system-windows/exitNonZero.bat new file mode 100644 index 00000000000..8204ab8df26 --- /dev/null +++ b/tests/fixtures/cases/system-windows/exitNonZero.bat @@ -0,0 +1,6 @@ +echo stdout +echo stderr 1>&2 + +exit 1 + +echo This should not appear diff --git a/tests/fixtures/cases/system-windows/exitZero.bat b/tests/fixtures/cases/system-windows/exitZero.bat new file mode 100644 index 00000000000..0cc40242e00 --- /dev/null +++ b/tests/fixtures/cases/system-windows/exitZero.bat @@ -0,0 +1,6 @@ +echo stdout +echo stderr 1>&2 + +exit 0 + +echo This should not appear diff --git a/tests/fixtures/cases/system-windows/passthroughArgs.bat b/tests/fixtures/cases/system-windows/passthroughArgs.bat new file mode 100644 index 00000000000..f00267f81c0 --- /dev/null +++ b/tests/fixtures/cases/system-windows/passthroughArgs.bat @@ -0,0 +1 @@ +echo %* diff --git a/tests/fixtures/cases/system-windows/project.yml b/tests/fixtures/cases/system-windows/project.yml new file mode 100644 index 00000000000..54a3c29c151 --- /dev/null +++ b/tests/fixtures/cases/system-windows/project.yml @@ -0,0 +1,45 @@ +tasks: + bat: + command: cmd + args: ./standard.bat + type: system + exitNonZero: + command: cmd + args: ./exitNonZero.bat + type: system + exitZero: + command: cmd.exe + args: ./exitZero.bat + type: system + passthroughArgs: + command: cmd + args: ./passthroughArgs.bat + type: system + envVars: + command: cmd + args: ./envVars.bat + env: + MOON_FOO: abc + MOON_BAR: '123' + MOON_BAZ: 'true' + type: system + envVarsMoon: + command: cmd.exe + args: ./envVarsMoon.bat + type: system + runFromProject: + command: cmd + args: ./cwd.bat + type: system + runFromWorkspace: + command: cmd.exe + args: ./system-windows/cwd.bat + type: system + options: + runFromWorkspaceRoot: true + retryCount: + command: cmd + args: ./exitNonZero.bat + type: system + options: + retryCount: 3 diff --git a/tests/fixtures/cases/system-windows/standard.bat b/tests/fixtures/cases/system-windows/standard.bat new file mode 100644 index 00000000000..3cbc31ab39b --- /dev/null +++ b/tests/fixtures/cases/system-windows/standard.bat @@ -0,0 +1,2 @@ +echo stdout +echo stderr 1>&2 diff --git a/tests/fixtures/cases/system/cwd.sh b/tests/fixtures/cases/system/cwd.sh new file mode 100644 index 00000000000..e18a15f7ab9 --- /dev/null +++ b/tests/fixtures/cases/system/cwd.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo "$PWD" diff --git a/tests/fixtures/cases/system/envVars.sh b/tests/fixtures/cases/system/envVars.sh new file mode 100644 index 00000000000..378e3e39957 --- /dev/null +++ b/tests/fixtures/cases/system/envVars.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +echo "MOON_FOO=$MOON_FOO" +echo "MOON_BAR=$MOON_BAR" +echo "MOON_BAZ=$MOON_BAZ" diff --git a/tests/fixtures/cases/system/envVarsMoon.sh b/tests/fixtures/cases/system/envVarsMoon.sh new file mode 100644 index 00000000000..fc4281c57e8 --- /dev/null +++ b/tests/fixtures/cases/system/envVarsMoon.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +for var in "${!MOON_@}"; do + if [[ "$var" != *"MOON_TEST"* ]];then + echo "$var=${!var}" + fi +done diff --git a/tests/fixtures/cases/system/exitNonZero.sh b/tests/fixtures/cases/system/exitNonZero.sh new file mode 100644 index 00000000000..680fa271cb7 --- /dev/null +++ b/tests/fixtures/cases/system/exitNonZero.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +echo "stdout" +echo "stderr" >&2 + +exit 1 + +echo "This should not appear!" diff --git a/tests/fixtures/cases/system/exitZero.sh b/tests/fixtures/cases/system/exitZero.sh new file mode 100644 index 00000000000..a837c59c005 --- /dev/null +++ b/tests/fixtures/cases/system/exitZero.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +echo "stdout" +echo "stderr" >&2 + +exit 0 + +echo "This should not appear!" diff --git a/tests/fixtures/cases/system/passthroughArgs.sh b/tests/fixtures/cases/system/passthroughArgs.sh new file mode 100644 index 00000000000..8629c24fc50 --- /dev/null +++ b/tests/fixtures/cases/system/passthroughArgs.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo "$@" diff --git a/tests/fixtures/cases/system/project.yml b/tests/fixtures/cases/system/project.yml new file mode 100644 index 00000000000..fe32bc6a7a4 --- /dev/null +++ b/tests/fixtures/cases/system/project.yml @@ -0,0 +1,53 @@ +tasks: + ls: + command: ls + args: '-1 .' + type: system + echo: + command: echo + args: 'hello' + type: system + bash: + command: bash + args: ./standard.sh + type: system + exitNonZero: + command: bash + args: ./exitNonZero.sh + type: system + exitZero: + command: bash + args: ./exitZero.sh + type: system + passthroughArgs: + command: bash + args: ./passthroughArgs.sh + type: system + envVars: + command: bash + args: ./envVars.sh + env: + MOON_FOO: abc + MOON_BAR: '123' + MOON_BAZ: 'true' + type: system + envVarsMoon: + command: bash + args: ./envVarsMoon.sh + type: system + runFromProject: + command: bash + args: ./cwd.sh + type: system + runFromWorkspace: + command: bash + args: ./system/cwd.sh + type: system + options: + runFromWorkspaceRoot: true + retryCount: + command: bash + args: ./exitNonZero.sh + type: system + options: + retryCount: 3 diff --git a/tests/fixtures/cases/system/standard.sh b/tests/fixtures/cases/system/standard.sh new file mode 100644 index 00000000000..a2ff505b11f --- /dev/null +++ b/tests/fixtures/cases/system/standard.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo "stdout" +echo "stderr" >&2 diff --git a/tests/fixtures/cases/target-scope-a/project.yml b/tests/fixtures/cases/target-scope-a/project.yml new file mode 100644 index 00000000000..8c79a625b67 --- /dev/null +++ b/tests/fixtures/cases/target-scope-a/project.yml @@ -0,0 +1,17 @@ +dependsOn: + - depsA + - depsB + - depsC + +tasks: + # :scope + all: + command: node + args: -e "console.log('scope=all')" + + # ^:scope + deps: + command: node + args: -e "console.log('scope=deps')" + deps: + - '^:standard' diff --git a/tests/fixtures/cases/target-scope-b/project.yml b/tests/fixtures/cases/target-scope-b/project.yml new file mode 100644 index 00000000000..d96b5b60bb3 --- /dev/null +++ b/tests/fixtures/cases/target-scope-b/project.yml @@ -0,0 +1,15 @@ +tasks: + # :scope + all: + command: node + args: -e "console.log('scope=all')" + + # ~:scope + self: + command: node + args: -e "console.log('scope=self')" + deps: + - '~:selfOther' + selfOther: + command: node + args: -e "console.log('scope=self/other')" diff --git a/tests/fixtures/cases/target-scope-c/project.yml b/tests/fixtures/cases/target-scope-c/project.yml new file mode 100644 index 00000000000..2b421f83667 --- /dev/null +++ b/tests/fixtures/cases/target-scope-c/project.yml @@ -0,0 +1,5 @@ +tasks: + # :scope + all: + command: node + args: -e "console.log('scope=all')" diff --git a/tests/fixtures/cases/tsconfig.json b/tests/fixtures/cases/tsconfig.json new file mode 100644 index 00000000000..08ba4599524 --- /dev/null +++ b/tests/fixtures/cases/tsconfig.json @@ -0,0 +1,7 @@ +{ + "references": [ + { + "path": "base" + } + ] +} diff --git a/tests/fixtures/init-sandbox/package.json b/tests/fixtures/init-sandbox/package.json new file mode 100644 index 00000000000..60b35c26b55 --- /dev/null +++ b/tests/fixtures/init-sandbox/package.json @@ -0,0 +1,4 @@ +{ + "name": "init", + "private": true +} diff --git a/tests/fixtures/node-npm/.moon/workspace.yml b/tests/fixtures/node-npm/.moon/workspace.yml new file mode 100644 index 00000000000..c31e412cec9 --- /dev/null +++ b/tests/fixtures/node-npm/.moon/workspace.yml @@ -0,0 +1,9 @@ +node: + # Use a unique version as to not collide with other tests + version: '16.1.0' + packageManager: 'npm' + npm: + version: '7.0.0' + +projects: + npm: npm diff --git a/tests/fixtures/node-npm/npm/project.yml b/tests/fixtures/node-npm/npm/project.yml new file mode 100644 index 00000000000..2ce8ea16f36 --- /dev/null +++ b/tests/fixtures/node-npm/npm/project.yml @@ -0,0 +1,7 @@ +tasks: + version: + command: npm + args: --version + installDep: + command: npm + args: install react@17.0.0 diff --git a/tests/fixtures/node-npm/package.json b/tests/fixtures/node-npm/package.json new file mode 100644 index 00000000000..a5b22ae5eb9 --- /dev/null +++ b/tests/fixtures/node-npm/package.json @@ -0,0 +1,7 @@ +{ + "name": "test-node-npm", + "private": true, + "workspaces": [ + "*" + ] +} diff --git a/tests/fixtures/node-pnpm/.moon/workspace.yml b/tests/fixtures/node-pnpm/.moon/workspace.yml new file mode 100644 index 00000000000..ed2851dabcc --- /dev/null +++ b/tests/fixtures/node-pnpm/.moon/workspace.yml @@ -0,0 +1,9 @@ +node: + # Use a unique version as to not collide with other tests + version: '16.2.0' + packageManager: 'pnpm' + pnpm: + version: '6.32.0' + +projects: + pnpm: pnpm diff --git a/tests/fixtures/node-pnpm/package.json b/tests/fixtures/node-pnpm/package.json new file mode 100644 index 00000000000..75d803f8ca7 --- /dev/null +++ b/tests/fixtures/node-pnpm/package.json @@ -0,0 +1,7 @@ +{ + "name": "test-node-pnpm", + "private": true, + "workspaces": [ + "*" + ] +} diff --git a/tests/fixtures/node-pnpm/pnpm/project.yml b/tests/fixtures/node-pnpm/pnpm/project.yml new file mode 100644 index 00000000000..d73e28360b2 --- /dev/null +++ b/tests/fixtures/node-pnpm/pnpm/project.yml @@ -0,0 +1,7 @@ +tasks: + version: + command: pnpm + args: --version + installDep: + command: pnpm + args: add react@17.0.0 diff --git a/tests/fixtures/node-yarn1/.moon/workspace.yml b/tests/fixtures/node-yarn1/.moon/workspace.yml new file mode 100644 index 00000000000..a387f85f6c2 --- /dev/null +++ b/tests/fixtures/node-yarn1/.moon/workspace.yml @@ -0,0 +1,9 @@ +node: + # Use a unique version as to not collide with other tests because of corepack + version: '16.3.0' + packageManager: 'yarn' + yarn: + version: '1.22.0' + +projects: + yarn: yarn diff --git a/tests/fixtures/node-yarn1/package.json b/tests/fixtures/node-yarn1/package.json new file mode 100644 index 00000000000..3e776175367 --- /dev/null +++ b/tests/fixtures/node-yarn1/package.json @@ -0,0 +1,5 @@ +{ + "name": "test-node-yarn1", + "private": true, + "workspaces": ["*"] +} diff --git a/tests/fixtures/node-yarn1/yarn/project.yml b/tests/fixtures/node-yarn1/yarn/project.yml new file mode 100644 index 00000000000..32c130cb8ed --- /dev/null +++ b/tests/fixtures/node-yarn1/yarn/project.yml @@ -0,0 +1,7 @@ +tasks: + version: + command: yarn + args: --version + installDep: + command: yarn + args: add -W --ignore-engines react@17.0.0 diff --git a/tests/fixtures/projects/.moon/workspace.yml b/tests/fixtures/projects/.moon/workspace.yml index 92e4fd620ac..2a9051e17e9 100644 --- a/tests/fixtures/projects/.moon/workspace.yml +++ b/tests/fixtures/projects/.moon/workspace.yml @@ -1,6 +1,5 @@ node: - # We use an old version to avoid conflicts within local dev - version: '10.0.0' + version: '16.0.0' projects: advanced: advanced diff --git a/tests/fixtures/projects/package.json b/tests/fixtures/projects/package.json index 0967ef424bc..9c545553b53 100644 --- a/tests/fixtures/projects/package.json +++ b/tests/fixtures/projects/package.json @@ -1 +1,4 @@ -{} +{ + "name": "test-projects", + "private": true +} diff --git a/tests/fixtures/tasks/.moon/workspace.yml b/tests/fixtures/tasks/.moon/workspace.yml index 0cc66f2c2bb..511e2d347c7 100644 --- a/tests/fixtures/tasks/.moon/workspace.yml +++ b/tests/fixtures/tasks/.moon/workspace.yml @@ -1,5 +1,4 @@ node: - # We use an old version to avoid conflicts within local dev - version: '10.0.0' + version: '16.0.0' projects: {} diff --git a/tests/fixtures/tasks/merge-append/project.yml b/tests/fixtures/tasks/merge-append/project.yml index 2ea552032fd..6b29692b04a 100644 --- a/tests/fixtures/tasks/merge-append/project.yml +++ b/tests/fixtures/tasks/merge-append/project.yml @@ -1,6 +1,6 @@ tasks: standard: - type: shell + type: system deps: - b:standard args: diff --git a/tests/fixtures/tasks/merge-prepend/project.yml b/tests/fixtures/tasks/merge-prepend/project.yml index 8fbae93e4be..9b3485079b7 100644 --- a/tests/fixtures/tasks/merge-prepend/project.yml +++ b/tests/fixtures/tasks/merge-prepend/project.yml @@ -1,6 +1,6 @@ tasks: standard: - type: shell + type: system command: newcmd deps: - b:standard diff --git a/tests/fixtures/tasks/merge-replace/project.yml b/tests/fixtures/tasks/merge-replace/project.yml index 37fc974c83d..20e3b5a915d 100644 --- a/tests/fixtures/tasks/merge-replace/project.yml +++ b/tests/fixtures/tasks/merge-replace/project.yml @@ -1,6 +1,6 @@ tasks: standard: - type: shell + type: system command: newcmd deps: - b:standard diff --git a/tests/fixtures/tasks/package.json b/tests/fixtures/tasks/package.json index 0967ef424bc..a5d3df7e5ea 100644 --- a/tests/fixtures/tasks/package.json +++ b/tests/fixtures/tasks/package.json @@ -1 +1,4 @@ -{} +{ + "name": "test-tasks", + "private": true +}