Skip to content

Commit

Permalink
fix: copy .gitignore files during project initialization
Browse files Browse the repository at this point in the history
  • Loading branch information
galargh committed Nov 18, 2024
1 parent 05ae2b3 commit 6dcfaa7
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 15 deletions.
60 changes: 50 additions & 10 deletions v-next/hardhat/src/internal/cli/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,32 @@ export async function ensureProjectPackageJson(
}
}

/**
* The following two functions are used to convert between relative workspace
* and template paths. To begin with, they are used to handle the special case
* of .gitignore.
*
* The reason for this is that npm ignores .gitignore files
* during npm pack (see https://github.com/npm/npm/issues/3763). That's why when
* we encounter a gitignore file in the template, we assume that it should be
* called .gitignore in the workspace (and vice versa).
*
* They are exported for testing purposes only.
*/

export function relativeWorkspaceToTemplatePath(file: string): string {
if (path.basename(file) === ".gitignore") {
return path.join(path.dirname(file), "gitignore");
}
return file;
}
export function relativeTemplateToWorkspacePath(file: string): string {
if (path.basename(file) === "gitignore") {
return path.join(path.dirname(file), ".gitignore");
}
return file;
}

/**
* copyProjectFiles copies the template files to the workspace.
*
Expand All @@ -280,27 +306,41 @@ export async function copyProjectFiles(
force?: boolean,
): Promise<void> {
// Find all the files in the workspace that would have been overwritten by the template files
const matchingFiles = await getAllFilesMatching(workspace, (file) =>
template.files.includes(path.relative(workspace, file)),
const matchingRelativeWorkspacePaths = await getAllFilesMatching(
workspace,
(file) => {
const relativeWorkspacePath = path.relative(workspace, file);
const relativeTemplatePath = relativeWorkspaceToTemplatePath(
relativeWorkspacePath,
);
return template.files.includes(relativeTemplatePath);
},
).then((files) => files.map((f) => path.relative(workspace, f)));

// Ask the user for permission to overwrite existing files if needed
if (matchingFiles.length !== 0) {
if (matchingRelativeWorkspacePaths.length !== 0) {
if (force === undefined) {
force = await promptForForce(matchingFiles);
force = await promptForForce(matchingRelativeWorkspacePaths);
}
}

// Copy the template files to the workspace
for (const file of template.files) {
if (force === false && matchingFiles.includes(file)) {
for (const relativeTemplatePath of template.files) {
const relativeWorkspacePath =
relativeTemplateToWorkspacePath(relativeTemplatePath);

if (
force === false &&
matchingRelativeWorkspacePaths.includes(relativeWorkspacePath)
) {
continue;
}
const pathToTemplateFile = path.join(template.path, file);
const pathToWorkspaceFile = path.join(workspace, file);

await ensureDir(path.dirname(pathToWorkspaceFile));
await copy(pathToTemplateFile, pathToWorkspaceFile);
const absoluteTemplatePath = path.join(template.path, relativeTemplatePath);
const absoluteWorkspacePath = path.join(workspace, relativeWorkspacePath);

await ensureDir(path.dirname(absoluteWorkspacePath));
await copy(absoluteTemplatePath, absoluteWorkspacePath);
}

console.log(`✨ ${chalk.cyan(`Template files copied`)} ✨`);
Expand Down
5 changes: 5 additions & 0 deletions v-next/hardhat/src/internal/cli/init/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ export async function getTemplates(): Promise<Template[]> {
if (f === pathToPackageJson) {
return false;
}
// .gitignore files are expected to be called gitignore in the templates
// because npm ignores .gitignore files during npm pack (see https://github.com/npm/npm/issues/3763)
if (path.basename(f) === ".gitignore") {
return false;
}
// We should ignore all the files according to the .gitignore rules
// However, for simplicity, we just ignore the node_modules folder
// If we needed to implement a more complex ignore logic, we could
Expand Down
9 changes: 9 additions & 0 deletions v-next/hardhat/templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,12 @@ The `package.json` file contains the template's metadata. The following fields a
Note that the `workspace:` prefix is stripped from the version of the template dependencies during project initialization.

The other template files are copied to the project during project initialization.

#### .gitignore files

Due to a limitation in npm, `.gitignore` files are always ignored during project packing/publishing (see https://github.com/npm/npm/issues/3763).

To work around this, we use the following convention:

- if a template file is named `gitignore`, it is copied to the project workspace as `.gitignore`;
- if a template file is named `.gitignore`, it is ignored during the project initialization (this should only affect local development, or future versions of `npm` if the aforementioned issue is resolved).
14 changes: 14 additions & 0 deletions v-next/hardhat/templates/mocha-ethers/gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Node modules
/node_modules

# Compilation output
/dist

# pnpm deploy output
/bundle

# Hardhat Build Artifacts
/artifacts

# Hardhat compilation (v2) support directory
/cache
14 changes: 14 additions & 0 deletions v-next/hardhat/templates/node-test-runner-viem/gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Node modules
/node_modules

# Compilation output
/dist

# pnpm deploy output
/bundle

# Hardhat Build Artifacts
/artifacts

# Hardhat compilation (v2) support directory
/cache
79 changes: 74 additions & 5 deletions v-next/hardhat/test/internal/cli/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
initHardhat,
installProjectDependencies,
printWelcomeMessage,
relativeTemplateToWorkspacePath,
relativeWorkspaceToTemplatePath,
} from "../../../../src/internal/cli/init/init.js";
import { getTemplates } from "../../../../src/internal/cli/init/template.js";

Expand Down Expand Up @@ -109,6 +111,36 @@ describe("ensureProjectPackageJson", () => {
});
});

describe("relativeWorkspaceToTemplatePath", () => {
it("should convert .gitignore to gitignore", () => {
assert.equal(relativeWorkspaceToTemplatePath(".gitignore"), "gitignore");
});
it("should not convert gitignore", () => {
assert.equal(relativeWorkspaceToTemplatePath("gitignore"), "gitignore");
});
it("should convert .gitignore to gitignore in a subdirectory", () => {
assert.equal(

Check failure on line 122 in v-next/hardhat/test/internal/cli/init/init.ts

View workflow job for this annotation

GitHub Actions / [hardhat] ci on ubuntu-latest (Node 22)

should convert .gitignore to gitignore in a subdirectory

AssertionError: Expected values to be strictly equal: + actual - expected + 'subdirectory/gitignore' - 'subdirectory/.gitignore' ^ - Expected + Received - subdirectory/.gitignore + subdirectory/gitignore at TestContext.<anonymous> (test/internal/cli/init/init.ts:122:12)

Check failure on line 122 in v-next/hardhat/test/internal/cli/init/init.ts

View workflow job for this annotation

GitHub Actions / [hardhat] ci on macos-13 (Node 22)

should convert .gitignore to gitignore in a subdirectory

AssertionError: Expected values to be strictly equal: + actual - expected + 'subdirectory/gitignore' - 'subdirectory/.gitignore' ^ - Expected + Received - subdirectory/.gitignore + subdirectory/gitignore at TestContext.<anonymous> (test/internal/cli/init/init.ts:122:12)

Check failure on line 122 in v-next/hardhat/test/internal/cli/init/init.ts

View workflow job for this annotation

GitHub Actions / [hardhat] ci on macos-latest (Node 22)

should convert .gitignore to gitignore in a subdirectory

AssertionError: Expected values to be strictly equal: + actual - expected + 'subdirectory/gitignore' - 'subdirectory/.gitignore' ^ - Expected + Received - subdirectory/.gitignore + subdirectory/gitignore at TestContext.<anonymous> (test/internal/cli/init/init.ts:122:12)
relativeWorkspaceToTemplatePath(path.join("subdirectory", "gitignore")),
path.join("subdirectory", ".gitignore"),
);
});
});

describe("relativeTemplateToWorkspacePath", () => {
it("should convert gitignore to .gitignore", () => {
assert.equal(relativeTemplateToWorkspacePath("gitignore"), ".gitignore");
});
it("should not convert .gitignore", () => {
assert.equal(relativeTemplateToWorkspacePath(".gitignore"), ".gitignore");
});
it("should convert gitignore to .gitignore in a subdirectory", () => {
assert.equal(
relativeTemplateToWorkspacePath(path.join("subdirectory", "gitignore")),
path.join("subdirectory", ".gitignore"),
);
});
});

describe("copyProjectFiles", () => {
useTmpDir("copyProjectFiles");

Expand All @@ -118,37 +150,71 @@ describe("copyProjectFiles", () => {
it("should copy the template files to the workspace and overwrite existing files", async () => {
const template = await getTemplate("mocha-ethers");
// Create template files with "some content" in the workspace
for (const file of template.files) {
const workspaceFiles = template.files.map(
relativeTemplateToWorkspacePath,
);
for (const file of workspaceFiles) {
const pathToFile = path.join(process.cwd(), file);
ensureDir(path.dirname(pathToFile));
await writeUtf8File(pathToFile, "some content");
}
// Copy the template files to the workspace
await copyProjectFiles(process.cwd(), template, true);
// Check that the template files in the workspace have been overwritten
for (const file of template.files) {
for (const file of workspaceFiles) {
const pathToFile = path.join(process.cwd(), file);
assert.notEqual(await readUtf8File(pathToFile), "some content");
}
});
it("should copy the .gitignore file correctly", async () => {
const template = await getTemplate("mocha-ethers");
// Copy the template files to the workspace
await copyProjectFiles(process.cwd(), template, true);
// Check that the .gitignore exists but gitignore does not
assert.ok(
await exists(path.join(process.cwd(), ".gitignore")),
".gitignore should exist",
);
assert.ok(
!(await exists(path.join(process.cwd(), "gitignore"))),
"gitignore should NOT exist",
);
});
});
describe("when force is false", () => {
it("should copy the template files to the workspace and NOT overwrite existing files", async () => {
const template = await getTemplate("mocha-ethers");
// Create template files with "some content" in the workspace
for (const file of template.files) {
const workspaceFiles = template.files.map(
relativeTemplateToWorkspacePath,
);
for (const file of workspaceFiles) {
const pathToFile = path.join(process.cwd(), file);
ensureDir(path.dirname(pathToFile));
await writeUtf8File(pathToFile, "some content");
}
// Copy the template files to the workspace
await copyProjectFiles(process.cwd(), template, false);
// Check that the template files in the workspace have not been overwritten
for (const file of template.files) {
for (const file of workspaceFiles) {
const pathToFile = path.join(process.cwd(), file);
assert.equal(await readUtf8File(pathToFile), "some content");
}
});
it("should copy the .gitignore file correctly", async () => {
const template = await getTemplate("mocha-ethers");
// Copy the template files to the workspace
await copyProjectFiles(process.cwd(), template, false);
// Check that the .gitignore exists but gitignore does not
assert.ok(
await exists(path.join(process.cwd(), ".gitignore")),
".gitignore should exist",
);
assert.ok(
!(await exists(path.join(process.cwd(), "gitignore"))),
"gitignore should NOT exist",
);
});
});
});

Expand Down Expand Up @@ -196,7 +262,10 @@ describe("initHardhat", async () => {
install: false,
});
assert.ok(await exists("package.json"), "package.json should exist");
for (const file of template.files) {
const workspaceFiles = template.files.map(
relativeTemplateToWorkspacePath,
);
for (const file of workspaceFiles) {
const pathToFile = path.join(process.cwd(), file);
assert.ok(await exists(pathToFile), `File ${file} should exist`);
}
Expand Down

0 comments on commit 6dcfaa7

Please sign in to comment.