Repository: NomicFoundation/hardhat-ignition (ts)

Issue:
Improve no deployment error message in status
If you run the status command with a non-existant deployment id you get:

![image](https://github.com/NomicFoundation/hardhat-ignition/assets/24030/9718cdd7-158f-48c8-973e-efdf54ce5689)

At the least the stack trace should be removed. Maybe a pattern to follow is HHs:

![image](https://github.com/NomicFoundation/hardhat-ignition/assets/24030/e5b002fc-11e8-4cad-91bd-9126a2c05b9a)


Pull request description:
remove support for module file names in tasks
We are removing support for invoking a module just by giving its file name. Instead the tasks will work in terms of a full or relative path to the module file.

Users prefer this for autocomplete reasons anyway, but it removes confusion as to whether it is the `ModuleId` or filename that is in use here.

Resolves #527.

Patch:
diff --git a/examples/complete/README.md b/examples/complete/README.md
index bc1240ad..60159b0d 100644
--- a/examples/complete/README.md
+++ b/examples/complete/README.md
@@ -7,7 +7,7 @@ This is a contrived example project, intended mostly to ensure working end-to-en
 To run the Ignition deploy against the ephemeral hardhat network:
 
 ```shell
-npx hardhat ignition deploy CompleteModule.js
+npx hardhat ignition deploy ./ignition/modules/CompleteModule.js
 ```
 
 ## Test
diff --git a/examples/ens/README.md b/examples/ens/README.md
index a86427d0..5fd01e9b 100644
--- a/examples/ens/README.md
+++ b/examples/ens/README.md
@@ -7,7 +7,7 @@ This Hardhat project is an example of using ignition to deploy the ENS system of
 To run the Ignition deploy against the ephemeral Hardhat network:
 
 ```shell
-npx hardhat ignition deploy ENS.js
+npx hardhat ignition deploy ./ignition/modules/ENS.js
 ```
 
 To run a deploy of ENS with a test registrar against the local node:
@@ -15,7 +15,7 @@ To run a deploy of ENS with a test registrar against the local node:
 ```shell
 npx hardhat node
 # In another terminal
-npx hardhat ignition deploy test-registrar.js --network localhost
+npx hardhat ignition deploy ./ignition/modules/test-registrar.js --network localhost
 ```
 
 ## Test
diff --git a/examples/sample/README.md b/examples/sample/README.md
index abb79ade..b8764c1a 100644
--- a/examples/sample/README.md
+++ b/examples/sample/README.md
@@ -7,7 +7,7 @@ This project is Hardhat's sample project enhanced with Hardhat Ignition.
 To run the Ignition deploy against the ephemeral hardhat network:
 
 ```shell
-npx hardhat ignition deploy LockModule.js
+npx hardhat ignition deploy ./ignition/modules/LockModule.js
 ```
 
 ## Test
diff --git a/examples/ts-sample/README.md b/examples/ts-sample/README.md
index ae0cd503..da79e563 100644
--- a/examples/ts-sample/README.md
+++ b/examples/ts-sample/README.md
@@ -8,7 +8,7 @@ Ignition.
 To run the Ignition deploy against the ephemeral Hardhat network:
 
 ```shell
-npx hardhat ignition deploy LockModule
+npx hardhat ignition deploy ./ignition/modules/LockModule
 ```
 
 ## Test
diff --git a/packages/hardhat-plugin/src/index.ts b/packages/hardhat-plugin/src/index.ts
index dd2d0f6f..dc9a1bd6 100644
--- a/packages/hardhat-plugin/src/index.ts
+++ b/packages/hardhat-plugin/src/index.ts
@@ -1,16 +1,17 @@
-import type {
+import {
   DeploymentParameters,
+  IgnitionError,
   StatusResult,
 } from "@nomicfoundation/ignition-core";
-
 import "@nomicfoundation/hardhat-ethers";
 import chalk from "chalk";
 import { readdirSync } from "fs-extra";
 import { extendConfig, extendEnvironment, scope } from "hardhat/config";
-import { lazyObject } from "hardhat/plugins";
+import { NomicLabsHardhatPluginError, lazyObject } from "hardhat/plugins";
 import path from "path";
 
 import "./type-extensions";
+import { shouldBeHardhatPluginError } from "./utils/shouldBeHardhatPluginError";
 
 /* ignition config defaults */
 const IGNITION_DIR = "ignition";
@@ -61,10 +62,7 @@ extendEnvironment((hre) => {
 
 ignitionScope
   .task("deploy")
-  .addPositionalParam(
-    "moduleNameOrPath",
-    "The name of the module file within the Ignition modules directory, or a path to the module file"
-  )
+  .addPositionalParam("modulePath", "The path to the module file to deploy")
   .addOptionalParam(
     "parameters",
     "A relative path to a JSON file to use for the module parameters,"
@@ -74,11 +72,11 @@ ignitionScope
   .setAction(
     async (
       {
-        moduleNameOrPath,
+        modulePath,
         parameters: parametersInput,
         id: givenDeploymentId,
       }: {
-        moduleNameOrPath: string;
+        modulePath: string;
         parameters?: string;
         id: string;
       },
@@ -116,10 +114,7 @@ ignitionScope
 
       await hre.run("compile", { quiet: true });
 
-      const userModule = loadModule(
-        hre.config.paths.ignition,
-        moduleNameOrPath
-      );
+      const userModule = loadModule(hre.config.paths.ignition, modulePath);
 
       if (userModule === undefined) {
         console.warn("No Ignition modules found");
@@ -173,17 +168,11 @@ ignitionScope
 ignitionScope
   .task("visualize")
   .addFlag("noOpen", "Disables opening report in browser")
-  .addPositionalParam(
-    "moduleNameOrPath",
-    "The name of the module file within the Ignition modules directory, or a path to the module file"
-  )
+  .addPositionalParam("modulePath", "The path to the module file to visualize")
   .setDescription("Visualize a module as an HTML report")
   .setAction(
     async (
-      {
-        noOpen = false,
-        moduleNameOrPath,
-      }: { noOpen: boolean; moduleNameOrPath: string },
+      { noOpen = false, modulePath }: { noOpen: boolean; modulePath: string },
       hre
     ) => {
       const { IgnitionModuleSerializer, batches } = await import(
@@ -198,10 +187,7 @@ ignitionScope
 
       await hre.run("compile", { quiet: true });
 
-      const userModule = loadModule(
-        hre.config.paths.ignition,
-        moduleNameOrPath
-      );
+      const userModule = loadModule(hre.config.paths.ignition, modulePath);
 
       if (userModule === undefined) {
         console.warn("No Ignition modules found");
@@ -247,7 +233,16 @@ ignitionScope
       id
     );
 
-    const statusResult = await status(deploymentDir);
+    let statusResult: StatusResult;
+    try {
+      statusResult = await status(deploymentDir);
+    } catch (e) {
+      if (e instanceof IgnitionError && shouldBeHardhatPluginError(e)) {
+        throw new NomicLabsHardhatPluginError("hardhat-ignition", e.message);
+      }
+
+      throw e;
+    }
 
     if (statusResult.started.length > 0) {
       console.log("");
@@ -334,7 +329,7 @@ ignitionScope
 ignitionScope
   .task("wipe")
   .addParam("id", "The id of the deployment that has the future to wipe")
-  .addParam("future", "The id of the future within the deploment to wipe")
+  .addParam("future", "The id of the future to wipe")
   .setDescription("Reset a deployment's future to allow rerunning")
   .setAction(
     async (
diff --git a/packages/hardhat-plugin/src/load-module.ts b/packages/hardhat-plugin/src/load-module.ts
index 74772636..f8838723 100644
--- a/packages/hardhat-plugin/src/load-module.ts
+++ b/packages/hardhat-plugin/src/load-module.ts
@@ -10,7 +10,7 @@ const MODULES_FOLDER = "modules";
 
 export function loadModule(
   ignitionDirectory: string,
-  moduleNameOrPath: string
+  modulePath: string
 ): IgnitionModule | undefined {
   const fullModulesDirectoryName = path.resolve(
     ignitionDirectory,
@@ -38,22 +38,19 @@ export function loadModule(
 
   debug(`Loading user modules from '${fullModulesDirectoryName}'`);
 
-  const fullpathToModule = resolveFullPathToModule(
-    fullModulesDirectoryName,
-    moduleNameOrPath
-  );
+  const fullpathToModule = path.resolve(modulePath);
 
-  if (fullpathToModule === undefined) {
+  if (!pathExistsSync(fullpathToModule)) {
     throw new HardhatPluginError(
       "hardhat-ignition",
-      `Could not find module ${moduleNameOrPath}`
+      `Could not find a module file at the path: ${modulePath}`
     );
   }
 
   if (!isInModuleDirectory(fullModulesDirectoryName, fullpathToModule)) {
     throw new HardhatPluginError(
       "hardhat-ignition",
-      `The referenced module ${moduleNameOrPath} is outside the module directory ${shortModulesDirectoryName}`
+      `The referenced module file ${modulePath} is outside the module directory ${shortModulesDirectoryName}`
     );
   }
 
@@ -64,40 +61,6 @@ export function loadModule(
   return module.default ?? module;
 }
 
-function resolveFullPathToModule(
-  modulesDirectory: string,
-  moduleNameOrPath: string
-): string | undefined {
-  const pathToModule = path.resolve(moduleNameOrPath);
-  if (pathExistsSync(pathToModule)) {
-    return pathToModule;
-  }
-
-  const relativeToModules = path.resolve(modulesDirectory, moduleNameOrPath);
-  if (pathExistsSync(relativeToModules)) {
-    return relativeToModules;
-  }
-
-  const relativeToModulesWithJsExtension = path.resolve(
-    modulesDirectory,
-    `${moduleNameOrPath}.js`
-  );
-  if (pathExistsSync(relativeToModulesWithJsExtension)) {
-    return relativeToModulesWithJsExtension;
-  }
-
-  const relativeToModulesWithTsExtension = path.resolve(
-    modulesDirectory,
-    `${moduleNameOrPath}.ts`
-  );
-
-  if (pathExistsSync(relativeToModulesWithTsExtension)) {
-    return relativeToModulesWithTsExtension;
-  }
-
-  return undefined;
-}
-
 function isInModuleDirectory(modulesDirectory: string, modulePath: string) {
   const resolvedModulesDirectory = path.resolve(modulesDirectory);
   const moduleRelativeToModuleDir = path.relative(
diff --git a/packages/hardhat-plugin/src/utils/shouldBeHardhatPluginError.ts b/packages/hardhat-plugin/src/utils/shouldBeHardhatPluginError.ts
new file mode 100644
index 00000000..9522f557
--- /dev/null
+++ b/packages/hardhat-plugin/src/utils/shouldBeHardhatPluginError.ts
@@ -0,0 +1,12 @@
+import { IgnitionError } from "@nomicfoundation/ignition-core";
+
+/* 
+  This is a whitelist of error codes that should be rethrown as NomicLabsHardhatPluginError.
+ */
+const whitelist = ["800"];
+
+export function shouldBeHardhatPluginError(error: IgnitionError): boolean {
+  const code = error.message.match(/IGN([0-9]+):/)![1];
+
+  return whitelist.includes(code);
+}
diff --git a/packages/ui/src/components/socials/index.tsx b/packages/ui/src/components/socials/index.tsx
index 2dc9b6c7..07454b5e 100644
--- a/packages/ui/src/components/socials/index.tsx
+++ b/packages/ui/src/components/socials/index.tsx
@@ -9,7 +9,7 @@ export enum SocialsEnum {
 }
 
 const SOCIALS_LINKS = {
-  [SocialsEnum.GITHUB]: "https://github.com/NomicFoundation/hardhat-ignition",
+  [SocialsEnum.GITHUB]: "https://hardhat.org/ignition",
   [SocialsEnum.TWITTER]: "https://twitter.com/HardhatHQ",
   [SocialsEnum.DISCORD]: "https://hardhat.org/ignition-discord",
 };
diff --git a/packages/ui/src/pages/visualization-overview/components/deployment-flow.tsx b/packages/ui/src/pages/visualization-overview/components/deployment-flow.tsx
index b3fb7eed..6aabdab0 100644
--- a/packages/ui/src/pages/visualization-overview/components/deployment-flow.tsx
+++ b/packages/ui/src/pages/visualization-overview/components/deployment-flow.tsx
@@ -6,10 +6,10 @@ import {
 import React, { useState } from "react";
 import { Tooltip } from "react-tooltip";
 import styled, { css } from "styled-components";
+import { TooltipIcon } from "../../../assets/TooltipIcon";
 import { Mermaid } from "../../../components/mermaid";
 import { getAllFuturesForModule } from "../../../queries/futures";
 import { toEscapedId } from "../../../utils/to-escaped-id";
-import { TooltipIcon } from "../../../assets/TooltipIcon";
 
 export const DeploymentFlow: React.FC<{
   ignitionModule: IgnitionModule<string, string, IgnitionModuleResult<string>>;
@@ -50,31 +50,43 @@ export const DeploymentFlow: React.FC<{
         Deployment flow <FlowTooltip />
       </SectionHeader>
 
-      <BatchBtnSection>
-        <VisualizeDiv>Visualize batches</VisualizeDiv>
-        {batches.map((_, i) => (
-          <BatchBtn
-            key={`batch-btn-${i}`}
-            onMouseEnter={() => setCurrentlyHovered(`batch-${i}`)}
-            onMouseLeave={() => setCurrentlyHovered("")}
-            isCurrentlyHovered={currentlyHovered === `batch-${i}`}
+      {futures.length <= 1 ? (
+        <SingleFutureNotice>
+          A module diagram will show once you have more than 1 future.
+        </SingleFutureNotice>
+      ) : (
+        <div>
+          <BatchBtnSection>
+            <VisualizeDiv>Visualize batches</VisualizeDiv>
+            {batches.map((_, i) => (
+              <BatchBtn
+                key={`batch-btn-${i}`}
+                onMouseEnter={() => setCurrentlyHovered(`batch-${i}`)}
+                onMouseLeave={() => setCurrentlyHovered("")}
+                isCurrentlyHovered={currentlyHovered === `batch-${i}`}
+              >
+                Batch <strong>#{i + 1}</strong>
+              </BatchBtn>
+            ))}
+          </BatchBtnSection>
+
+          <HighlightedFutures
+            futures={futuresToHighlight}
+            deploys={deploys}
+            others={others}
           >
-            Batch <strong>#{i + 1}</strong>
-          </BatchBtn>
-        ))}
-      </BatchBtnSection>
-
-      <HighlightedFutures
-        futures={futuresToHighlight}
-        deploys={deploys}
-        others={others}
-      >
-        <Mermaid ignitionModule={ignitionModule} />
-      </HighlightedFutures>
+            <Mermaid ignitionModule={ignitionModule} />
+          </HighlightedFutures>
+        </div>
+      )}
     </div>
   );
 };
 
+const SingleFutureNotice = styled.div`
+  padding-top: 1rem;
+`;
+
 const VisualizeDiv = styled.div`
   font-weight: 700;
   padding: 1.5rem;
diff --git a/packages/ui/src/pages/visualization-overview/components/execution-batches.tsx b/packages/ui/src/pages/visualization-overview/components/execution-batches.tsx
index 8f53b7ad..cb6b83a7 100644
--- a/packages/ui/src/pages/visualization-overview/components/execution-batches.tsx
+++ b/packages/ui/src/pages/visualization-overview/components/execution-batches.tsx
@@ -41,17 +41,15 @@ export const ExecutionBatches: React.FC<{
 
   const [toggleState, setToggledInternal] = useState(toggleMap);
 
-  const setToggled = (id: string) => {
-    const newToggleStatus = !toggleState[id];
-
-    if (newToggleStatus) {
+  const setToggled = (id: string, newToggleState: boolean) => {
+    if (newToggleState) {
       scrollRefMap.current[id].current?.scrollIntoView({
         behavior: "smooth",
         block: "center",
       });
     }
 
-    const newState = { ...toggleState, [id]: newToggleStatus };
+    const newState = { ...toggleState, [id]: newToggleState };
     setToggledInternal(newState);
   };
 
@@ -125,10 +123,10 @@ const BatchesTooltip: React.FC = () => (
       </div>
       <br />
       <div>
-        The sequence order shown for each batch doesn&apos;t reflect the final execution order.
-        The exact order is determined once they&apos;re run. However, this
-        specific order isn&apos;t relevant to the process, allowing for
-        simultaneous execution.
+        The sequence order shown for each batch doesn&apos;t reflect the final
+        execution order. The exact order is determined once they&apos;re run.
+        However, this specific order isn&apos;t relevant to the process,
+        allowing for simultaneous execution.
       </div>
     </Tooltip>
   </span>
diff --git a/packages/ui/src/pages/visualization-overview/components/future-batch.tsx b/packages/ui/src/pages/visualization-overview/components/future-batch.tsx
index a93d04b9..7313bac6 100644
--- a/packages/ui/src/pages/visualization-overview/components/future-batch.tsx
+++ b/packages/ui/src/pages/visualization-overview/components/future-batch.tsx
@@ -14,7 +14,7 @@ export const FutureBatch: React.FC<{
   batch: Future[];
   index: number;
   toggleState: Record<string, boolean>;
-  setToggled: (id: string) => void;
+  setToggled: (id: string, newToggleState: boolean) => void;
   setCurrentlyHovered: (id: string) => void;
   setHoveredFuture: (id: string) => void;
   scrollRefMap: Record<string, React.RefObject<HTMLDivElement>>;
@@ -91,7 +91,7 @@ const FutureBtn = styled.div<{ isLibrary: boolean; toggled: boolean }>`
 const FutureBlock: React.FC<{
   future: Future;
   toggleState: Record<string, boolean>;
-  setToggled: (id: string) => void;
+  setToggled: (id: string, newToggleState: boolean) => void;
   setCurrentlyHovered: (id: string) => void;
   setHoveredFuture: (id: string) => void;
   classKey: string;
@@ -124,7 +124,7 @@ const FutureBlock: React.FC<{
         className={`${className} ${classKey}`}
         isLibrary={isLibrary}
         toggled={toggled}
-        onClick={() => setToggled(futureId)}
+        onClick={() => setToggled(futureId, !toggled)}
       >
         <FutureHeader
           isLibrary={isLibrary}
@@ -206,7 +206,7 @@ const FutureDetailsStyle = styled.div`
 const FutureDetailsSection: React.FC<{
   className: string;
   future: Future;
-  setToggled: (id: string) => void;
+  setToggled: (id: string, newToggleState: boolean) => void;
   setHoveredFuture: (id: string) => void;
 }> = ({ className, future, setToggled, setHoveredFuture }) => {
   switch (future.type) {
@@ -324,7 +324,7 @@ const FutureDetailsSection: React.FC<{
 };
 
 const Argument: React.FC<{
-  setToggled: (id: string) => void;
+  setToggled: (id: string, newToggleState: boolean) => void;
   setHoveredFuture: (id: string) => void;
   arg: ArgumentType;
 }> = ({ setToggled, arg, setHoveredFuture }) => {
@@ -337,7 +337,7 @@ const Argument: React.FC<{
           cursor: "pointer",
         }}
         className="future-argument"
-        onClick={() => setToggled(arg.id)}
+        onClick={() => setToggled(arg.id, true)}
         onMouseEnter={() => setHoveredFuture(arg.id)}
         onMouseLeave={() => setHoveredFuture("")}
       >
diff --git a/packages/ui/src/pages/visualization-overview/visualization-overview.tsx b/packages/ui/src/pages/visualization-overview/visualization-overview.tsx
index 4394b24f..9d4db679 100644
--- a/packages/ui/src/pages/visualization-overview/visualization-overview.tsx
+++ b/packages/ui/src/pages/visualization-overview/visualization-overview.tsx
@@ -24,10 +24,7 @@ export const VisualizationOverview: React.FC<{
 
         <span style={{ display: "flex", alignItems: "center" }}>
           <DocLink>
-            <a
-              href="https://github.com/NomicFoundation/hardhat-ignition"
-              target="_blank"
-            >
+            <a href="https://hardhat.org/ignition/docs" target="_blank">
               DOCUMENTATION <ExternalLinkIcon />
             </a>
           </DocLink>


Test patch:
diff --git a/packages/core/test-integrations/new-api/internal/reconciliation/chainId.ts b/packages/core/test-integrations/new-api/internal/reconciliation/chainId.ts
index af9e820f..888eb4be 100644
--- a/packages/core/test-integrations/new-api/internal/reconciliation/chainId.ts
+++ b/packages/core/test-integrations/new-api/internal/reconciliation/chainId.ts
@@ -14,7 +14,7 @@ describe("chainId reconciliation", function () {
       this.hre.run(
         { scope: "ignition", task: "deploy" },
         {
-          moduleNameOrPath: "./ignition/modules/LockModule.js",
+          modulePath: "./ignition/modules/LockModule.js",
         }
       ),
       /Previous chain id: 123\. Current chain id: 31337/
diff --git a/packages/hardhat-plugin/test/load-module.ts b/packages/hardhat-plugin/test/load-module.ts
index b80f3782..eadd00bd 100644
--- a/packages/hardhat-plugin/test/load-module.ts
+++ b/packages/hardhat-plugin/test/load-module.ts
@@ -8,40 +8,22 @@ import { useEphemeralIgnitionProject } from "./use-ignition-project";
 describe("loadModule", function () {
   useEphemeralIgnitionProject("user-modules");
 
-  it("should return the module given the module name", () => {
-    const module = loadModule("ignition", "TestModule");
-
-    if (module === undefined) {
-      assert.fail("Module was not loaded");
-    }
-
-    assert.equal(module.id, "testing123");
-  });
-
-  it("should return the module given the module name and extension", () => {
-    const module = loadModule("ignition", "TestModule.js");
-
-    if (module === undefined) {
-      assert.fail("Module was not loaded");
-    }
-
-    assert.equal(module.id, "testing123");
-  });
-
-  it("should throw if the module name does not exist", () => {
-    assert.throws(() => loadModule("ignition", "Fake"));
-  });
-
-  it("should throw if the module name with extension does not exist", () => {
-    assert.throws(() => loadModule("ignition", "Fake.js"));
+  it("should throw if given a user module directory that does not exist", async () => {
+    assert.throws(
+      () => loadModule("/fake", "AFile.js"),
+      `Ignition directory /fake not found.`
+    );
   });
 
   it("should throw if the full path to the module does not exist", () => {
-    assert.throws(() => loadModule("ignition", "./ignition/Fake.js"));
+    assert.throws(
+      () => loadModule("ignition", "./ignition/modules/Fake.js"),
+      "Could not find a module file at the path: ./ignition/modules/Fake.js"
+    );
   });
 
   it("should throw if the full path to the module is outside the module directory", () => {
-    const unixErrorMessage = `The referenced module ./hardhat.config.js is outside the module directory ignition/modules`;
+    const unixErrorMessage = `The referenced module file ./hardhat.config.js is outside the module directory ignition/modules`;
 
     const expectedErrorMessage =
       process.platform === "win32"
@@ -53,11 +35,4 @@ describe("loadModule", function () {
       expectedErrorMessage
     );
   });
-
-  it("should throw if given a user module directory that does not exist", async () => {
-    assert.throws(
-      () => loadModule("/fake", "AFile.js"),
-      `Ignition directory /fake not found.`
-    );
-  });
 });
diff --git a/packages/hardhat-plugin/test/plan/index.ts b/packages/hardhat-plugin/test/plan/index.ts
index 56da1307..9d3a085e 100644
--- a/packages/hardhat-plugin/test/plan/index.ts
+++ b/packages/hardhat-plugin/test/plan/index.ts
@@ -20,7 +20,7 @@ describe("visualize", () => {
       },
       {
         noOpen: true,
-        moduleNameOrPath: "MyModule.js",
+        modulePath: "./ignition/modules/MyModule.js",
       }
     );