diff --git a/.changeset/add-cli-gitignore-helpers.md b/.changeset/add-cli-gitignore-helpers.md new file mode 100644 index 0000000000..702f4b0e43 --- /dev/null +++ b/.changeset/add-cli-gitignore-helpers.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/cli": minor +--- + +Add gitignore helpers for appending Wrangler-related entries + +New `maybeAppendWranglerToGitIgnore` and `maybeAppendWranglerToGitIgnoreLikeFile` functions that automatically append Wrangler-related entries (`.wrangler`, `.dev.vars*`, `.env*`, and their negated example patterns) to `.gitignore` or similar ignore files. Existing entries are detected and skipped to avoid duplicates. diff --git a/packages/cli/__tests__/gitignore.test.ts b/packages/cli/__tests__/gitignore.test.ts new file mode 100644 index 0000000000..9bd61e1d66 --- /dev/null +++ b/packages/cli/__tests__/gitignore.test.ts @@ -0,0 +1,449 @@ +import { appendFileSync, existsSync, statSync, writeFileSync } from "node:fs"; +import { readFileSync } from "@cloudflare/workers-utils"; +import { beforeEach, describe, test, vi } from "vitest"; +import { + maybeAppendWranglerToGitIgnore, + maybeAppendWranglerToGitIgnoreLikeFile, +} from "../gitignore"; +import type { PathLike } from "node:fs"; +import type { Mock } from "vitest"; + +vi.mock("node:fs"); +vi.mock("@cloudflare/workers-utils"); +vi.mock("../interactive", () => ({ + spinner: () => ({ + start: vi.fn(), + stop: vi.fn(), + update: vi.fn(), + }), +})); + +describe("maybeAppendWranglerToGitIgnoreLikeFile", () => { + let writeFileSyncMock: Mock; + let appendFileSyncMock: Mock; + + beforeEach(() => { + vi.resetAllMocks(); + + writeFileSyncMock = vi.mocked(writeFileSync); + appendFileSyncMock = vi.mocked(appendFileSync); + }); + + test("should append the wrangler section to a standard gitignore file", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .vscode` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + + # wrangler files + .wrangler + .dev.vars* + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + test("should not touch the file if it already contains all wrangler files", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .dev.vars* + !.dev.vars.example + .env* + !.env.example + .vscode + .wrangler + ` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock).not.toHaveBeenCalled(); + }); + + test("should not touch the file if it contains all wrangler files (and can cope with comments)", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .wrangler # This is for wrangler + .dev.vars* # this is for wrangler and getPlatformProxy + !.dev.vars.example # more comments + .env* # even more + !.env.example # and a final one + .vscode + ` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock).not.toHaveBeenCalled(); + }); + + test("should append to the file the missing wrangler files when some are already present (should add the section heading if including .wrangler and some others)", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .dev.vars* + .vscode` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + + # wrangler files + .wrangler + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + test("should append to the file the missing wrangler files when some are already present (should not add the section heading if .wrangler already exists)", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .wrangler + .dev.vars* + .vscode` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + test("should append to the file the missing wrangler files when some are already present (should not add the section heading if only adding .wrangler)", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .dev.vars* + !.dev.vars.example + .env* + !.env.example + .vscode` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + + .wrangler + ", + ], + ] + `); + }); + + test("when it appends to the file it doesn't include an empty line only if there was one already", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .dev.vars* + !.dev.vars.example + .env* + !.env.example + .vscode + + ` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + .wrangler + ", + ], + ] + `); + }); + + test("should create the file if it didn't exist already", ({ expect }) => { + mockIgnoreFile("my-project/.gitignore", ""); + // pretend that the file doesn't exist + vi.mocked(existsSync).mockImplementation(() => false); + + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + // writeFile wrote the (empty) file + expect(writeFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + "", + ], + ] + `); + + // and the correct lines were then added to it + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + "# wrangler files + .wrangler + .dev.vars* + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + test("should add the wildcard .dev.vars* entry even if a .dev.vars is already included", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .dev.vars + .vscode + ` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + # wrangler files + .wrangler + .dev.vars* + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + test("should not add the .env entries if some form of .env entries are already included", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + .env + .env.* + !.env.example + ` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + # wrangler files + .wrangler + .dev.vars* + !.dev.vars.example + ", + ], + ] + `); + }); + + test("should not add the .wrangler entry if a .wrangler/ is already included", ({ + expect, + }) => { + mockIgnoreFile( + "my-project/.gitignore", + ` + node_modules + .wrangler/ # This is for wrangler + .vscode + ` + ); + maybeAppendWranglerToGitIgnoreLikeFile("my-project/.gitignore"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + .dev.vars* + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + function mockIgnoreFile(path: string, content: string) { + vi.mocked(existsSync).mockImplementation( + (filePath: PathLike) => filePath === path + ); + vi.mocked(readFileSync).mockImplementation((filePath: string) => + filePath === path ? content.replace(/\n\s*/g, "\n") : "" + ); + } +}); + +describe("maybeAppendWranglerToGitIgnore", () => { + let appendFileSyncMock: Mock; + + beforeEach(() => { + vi.resetAllMocks(); + + appendFileSyncMock = vi.mocked(appendFileSync); + + vi.mocked(statSync).mockImplementation( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (path: string) => ({ + isDirectory() { + return path.endsWith(".git"); + }, + }) + ); + }); + + test("should not create the gitignore file if neither the .git directory not the .gitingore file exist", ({ + expect, + }) => { + // no .gitignore file exists + vi.mocked(existsSync).mockImplementation(() => false); + // neither a .git directory does + vi.mocked(statSync).mockImplementation(() => { + throw Object.assign(new Error("ENOENT"), { code: "ENOENT" }); + }); + + maybeAppendWranglerToGitIgnore("my-project"); + + expect(vi.mocked(writeFileSync)).not.toHaveBeenCalled(); + expect(appendFileSyncMock).not.toHaveBeenCalled(); + }); + + test("should create a .gitignore file when .git directory exists", ({ + expect, + }) => { + // no .gitignore file exists + vi.mocked(existsSync).mockImplementation(() => false); + vi.mocked(readFileSync).mockImplementation(() => ""); + + maybeAppendWranglerToGitIgnore("my-project"); + + // writeFileSync created the (empty) file + expect(vi.mocked(writeFileSync).mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + "", + ], + ] + `); + + // and the correct wrangler lines were appended to it + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + "# wrangler files + .wrangler + .dev.vars* + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); + + test("should append wrangler entries when the .gitignore file exists but the .git directory does not", ({ + expect, + }) => { + // .gitignore exists + vi.mocked(existsSync).mockImplementation( + (filePath: PathLike) => filePath === "my-project/.gitignore" + ); + vi.mocked(readFileSync).mockImplementation((filePath: string) => + filePath === "my-project/.gitignore" ? "\nnode_modules\n.vscode\n" : "" + ); + // .git directory does NOT exist + vi.mocked(statSync).mockImplementation(() => { + throw Object.assign(new Error("ENOENT"), { code: "ENOENT" }); + }); + + maybeAppendWranglerToGitIgnore("my-project"); + + expect(appendFileSyncMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "my-project/.gitignore", + " + # wrangler files + .wrangler + .dev.vars* + !.dev.vars.example + .env* + !.env.example + ", + ], + ] + `); + }); +}); diff --git a/packages/wrangler/src/autoconfig/c3-vendor/add-wrangler-gitignore.ts b/packages/cli/gitignore.ts similarity index 57% rename from packages/wrangler/src/autoconfig/c3-vendor/add-wrangler-gitignore.ts rename to packages/cli/gitignore.ts index 9b81505f03..65c67da15c 100644 --- a/packages/wrangler/src/autoconfig/c3-vendor/add-wrangler-gitignore.ts +++ b/packages/cli/gitignore.ts @@ -1,37 +1,26 @@ import { appendFileSync, existsSync, statSync, writeFileSync } from "node:fs"; -import { brandColor, dim } from "@cloudflare/cli/colors"; -import { spinner } from "@cloudflare/cli/interactive"; +import { basename } from "node:path"; import { readFileSync } from "@cloudflare/workers-utils"; - -const directoryExists = (path: string): boolean => { - try { - const stat = statSync(path); - return stat.isDirectory(); - } catch (error) { - if ((error as { code: string }).code === "ENOENT") { - return false; - } - throw new Error(error as string); - } -}; - -export const addWranglerToGitIgnore = (projectPath: string) => { - const gitIgnorePath = `${projectPath}/.gitignore`; - const gitIgnorePreExisted = existsSync(gitIgnorePath); - - const gitDirExists = directoryExists(`${projectPath}/.git`); - - if (!gitIgnorePreExisted && !gitDirExists) { - // if there is no .gitignore file and neither a .git directory - // then bail as the project is likely not targeting/using git - return; - } - - if (!gitIgnorePreExisted) { - writeFileSync(gitIgnorePath, ""); +import { brandColor, dim } from "./colors"; +import { spinner } from "./interactive"; + +/** + * Appends to a file that follows a .gitignore-like structure any missing + * Wrangler-related entries (`.wrangler`, `.dev.vars*`, `.env*`, and their + * negated example patterns). + * + * Creates the file if it does not already exist. + * + * @param filePath Absolute or relative path to the ignore file to update. + */ +export function maybeAppendWranglerToGitIgnoreLikeFile(filePath: string): void { + const filePreExisted = existsSync(filePath); + + if (!filePreExisted) { + writeFileSync(filePath, ""); } - const existingGitIgnoreContent = readFileSync(gitIgnorePath); + const existingGitIgnoreContent = readFileSync(filePath); const wranglerGitIgnoreFilesToAdd: string[] = []; const hasDotWrangler = existingGitIgnoreContent.match( @@ -90,10 +79,10 @@ export const addWranglerToGitIgnore = (projectPath: string) => { } const s = spinner(); - s.start("Adding Wrangler files to the .gitignore file"); + s.start(`Adding Wrangler files to the ${basename(filePath)} file`); const linesToAppend = [ - ...(gitIgnorePreExisted + ...(filePreExisted ? ["", ...(!existingGitIgnoreContent.match(/\n\s*$/) ? [""] : [])] : []), ]; @@ -106,11 +95,57 @@ export const addWranglerToGitIgnore = (projectPath: string) => { linesToAppend.push(""); - appendFileSync(gitIgnorePath, linesToAppend.join("\n")); + appendFileSync(filePath, linesToAppend.join("\n")); + + const fileName = basename(filePath); s.stop( - `${brandColor(gitIgnorePreExisted ? "updated" : "created")} ${dim( - ".gitignore file" + `${brandColor(filePreExisted ? "updated" : "created")} ${dim( + `${fileName} file` )}` ); -}; +} + +/** + * Appends any missing Wrangler-related entries to the project's `.gitignore` file. + * + * Bails out only when *neither* a `.gitignore` file nor a `.git` directory exists, + * which indicates the project is likely not targeting/using git. If either one is + * present the entries are appended (creating `.gitignore` if needed). + * + * @param projectPath Root directory of the project. + */ +export function maybeAppendWranglerToGitIgnore(projectPath: string): void { + const gitIgnorePath = `${projectPath}/.gitignore`; + const gitIgnorePreExisted = existsSync(gitIgnorePath); + const gitDirExists = directoryExists(`${projectPath}/.git`); + + if (!gitIgnorePreExisted && !gitDirExists) { + // if there is no .gitignore file and neither a .git directory + // then bail as the project is likely not targeting/using git + return; + } + + maybeAppendWranglerToGitIgnoreLikeFile(gitIgnorePath); +} + +/** + * Checks whether a directory exists at the given path. + * + * Returns `false` when the path does not exist (`ENOENT`). Re-throws any + * other filesystem error. + * + * @param path Path to check + * @returns `true` if a directory exists at `path`, `false` otherwise + */ +function directoryExists(path: string): boolean { + try { + const stat = statSync(path); + return stat.isDirectory(); + } catch (error) { + if ((error as { code: string }).code === "ENOENT") { + return false; + } + throw new Error(error as string); + } +} diff --git a/packages/create-cloudflare/src/__tests__/templates.test.ts b/packages/create-cloudflare/src/__tests__/templates.test.ts index a8ada75e50..518a48815b 100644 --- a/packages/create-cloudflare/src/__tests__/templates.test.ts +++ b/packages/create-cloudflare/src/__tests__/templates.test.ts @@ -1,26 +1,17 @@ -import { existsSync, statSync } from "node:fs"; +import { existsSync } from "node:fs"; import { join } from "node:path"; import { spinner } from "@cloudflare/cli/interactive"; import degit from "degit"; import { mockSpinner } from "helpers/__tests__/mocks"; -import { - appendFile, - directoryExists, - readFile, - readJSON, - writeFile, - writeJSON, -} from "helpers/files"; +import { readFile, readJSON, writeFile, writeJSON } from "helpers/files"; import { beforeEach, describe, test, vi } from "vitest"; import { getAgentsMd } from "../agents-md"; import { - addWranglerToGitIgnore, deriveCorrelatedArgs, downloadRemoteTemplate, updatePackageName, writeAgentsMd, } from "../templates"; -import type { PathLike } from "node:fs"; import type { C3Args, C3Context } from "types"; import type { Mock } from "vitest"; @@ -29,383 +20,6 @@ vi.mock("fs"); vi.mock("helpers/files"); vi.mock("@cloudflare/cli/interactive"); -describe("addWranglerToGitIgnore", () => { - let writeFileMock: Mock; - let appendFileMock: Mock; - - beforeEach(() => { - vi.resetAllMocks(); - mockSpinner(); - - writeFileMock = vi.mocked(writeFile); - appendFileMock = vi.mocked(appendFile); - - vi.mocked(statSync).mockImplementation( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - (path: string) => ({ - isDirectory() { - return path.endsWith(".git"); - }, - }) - ); - }); - - test("should append the wrangler section to a standard gitignore file", ({ - expect, - }) => { - mockGitIgnore( - "my-project/.gitignore", - ` - node_modules - .vscode` - ); - addWranglerToGitIgnore({ - project: { path: "my-project" }, - } as unknown as C3Context); - - expect(appendFileMock.mock.calls).toMatchInlineSnapshot(` - [ - [ - "my-project/.gitignore", - " - - # wrangler files - .wrangler - .dev.vars* - !.dev.vars.example - .env* - !.env.example - ", - ], - ] - `); - }); - test("should not touch the gitignore file if it already contains all wrangler files", ({ - expect, - }) => { - mockGitIgnore( - "my-project/.gitignore", - ` - node_modules - .dev.vars* - !.dev.vars.example - .env* - !.env.example - .vscode - .wrangler - ` - ); - addWranglerToGitIgnore({ - project: { path: "my-project" }, - } as unknown as C3Context); - - expect(appendFileMock).not.toHaveBeenCalled(); - }); - - test("should not touch the gitignore file if contains all wrangler files (and can cope with comments)", ({ - expect, - }) => { - mockGitIgnore( - "my-project/.gitignore", - ` - node_modules - .wrangler # This is for wrangler - .dev.vars* # this is for wrangler and getPlatformProxy - !.dev.vars.example # more comments - .env* # even more - !.env.example # and a final one - .vscode - ` - ); - addWranglerToGitIgnore({ - project: { path: "my-project" }, - } as unknown as C3Context); - - expect(appendFileMock).not.toHaveBeenCalled(); - }); - - test("should append to the gitignore file the missing wrangler files when some are already present (should add the section heading if including .wrangler and some others)", ({ - expect, - }) => { - mockGitIgnore( - "my-project/.gitignore", - ` - node_modules - .dev.vars* - .vscode` - ); - addWranglerToGitIgnore({ - project: { path: "my-project" }, - } as unknown as C3Context); - - expect(appendFileMock.mock.calls).toMatchInlineSnapshot(` - [ - [ - "my-project/.gitignore", - " - - # wrangler files - .wrangler - !.dev.vars.example - .env* - !.env.example - ", - ], - ] - `); - }); - - test("should append to the gitignore file the missing wrangler files when some are already present (should not add the section heading if .wrangler already exists)", ({ - expect, - }) => { - mockGitIgnore( - "my-project/.gitignore", - ` - node_modules - .wrangler - .dev.vars* - .vscode` - ); - addWranglerToGitIgnore({ - project: { path: "my-project" }, - } as unknown as C3Context); - - expect(appendFileMock.mock.calls).toMatchInlineSnapshot(` - [ - [ - "my-project/.gitignore", - " - - !.dev.vars.example - .env* - !.env.example - ", - ], - ] - `); - }); - - test("should append to the gitignore file the missing wrangler files when some are already present (should not add the section heading if only adding .wrangler)", ({ - expect, - }) => { - mockGitIgnore( - "my-project/.gitignore", - ` - node_modules - .dev.vars* - !.dev.vars.example - .env* - !.env.example - .vscode` - ); - addWranglerToGitIgnore({ - project: { path: "my-project" }, - } as unknown as C3Context); - - expect(appendFileMock.mock.calls).toMatchInlineSnapshot(` - [ - [ - "my-project/.gitignore", - " - - .wrangler - ", - ], - ] - `); - }); - - test("when it appends to the gitignore file it doesn't include an empty line only if there was one already", ({ - expect, - }) => { - mockGitIgnore( - "my-project/.gitignore", - ` - node_modules - .dev.vars* - !.dev.vars.example - .env* - !.env.example - .vscode - - ` - ); - addWranglerToGitIgnore({ - project: { path: "my-project" }, - } as unknown as C3Context); - - expect(appendFileMock.mock.calls).toMatchInlineSnapshot(` - [ - [ - "my-project/.gitignore", - " - .wrangler - ", - ], - ] - `); - }); - - test("should create the gitignore file if it didn't exist already", ({ - expect, - }) => { - // let's mock a gitignore file to be read by readFile - mockGitIgnore("my-project/.gitignore", ""); - // but let's pretend that it doesn't exist - vi.mocked(existsSync).mockImplementation(() => false); - // let's also pretend that the .git directory exists - vi.mocked(directoryExists).mockImplementation(() => true); - - addWranglerToGitIgnore({ - project: { path: "my-project" }, - } as unknown as C3Context); - - // writeFile wrote the (empty) gitignore file - expect(writeFileMock.mock.calls).toMatchInlineSnapshot(` - [ - [ - "my-project/.gitignore", - "", - ], - ] - `); - - // and the correct lines were then added to it - expect(appendFileMock.mock.calls).toMatchInlineSnapshot(` - [ - [ - "my-project/.gitignore", - " - - # wrangler files - .wrangler - .dev.vars* - !.dev.vars.example - .env* - !.env.example - ", - ], - ] - `); - }); - - test("should not create the gitignore file the project doesn't use git", ({ - expect, - }) => { - // no .gitignore file exists - vi.mocked(existsSync).mockImplementation(() => false); - // neither a .git directory does - vi.mocked(directoryExists).mockImplementation(() => false); - - addWranglerToGitIgnore({ - project: { path: "my-project" }, - } as unknown as C3Context); - - expect(writeFileMock).not.toHaveBeenCalled(); - }); - - test("should add the wildcard .dev.vars* entry even if a .dev.vars is already included", ({ - expect, - }) => { - mockGitIgnore( - "my-project/.gitignore", - ` - node_modules - .dev.vars - .vscode - ` - ); - addWranglerToGitIgnore({ - project: { path: "my-project" }, - } as unknown as C3Context); - - expect(appendFileMock.mock.calls).toMatchInlineSnapshot(` - [ - [ - "my-project/.gitignore", - " - # wrangler files - .wrangler - .dev.vars* - !.dev.vars.example - .env* - !.env.example - ", - ], - ] - `); - }); - - test("should not add the .env entries if some form of .env entries are already included", ({ - expect, - }) => { - mockGitIgnore( - "my-project/.gitignore", - ` - .env - .env.* - !.env.example - ` - ); - addWranglerToGitIgnore({ - project: { path: "my-project" }, - } as unknown as C3Context); - - expect(appendFileMock.mock.calls).toMatchInlineSnapshot(` - [ - [ - "my-project/.gitignore", - " - # wrangler files - .wrangler - .dev.vars* - !.dev.vars.example - ", - ], - ] - `); - }); - - test("should not add the .wrangler entry if a .wrangler/ is already included)", ({ - expect, - }) => { - mockGitIgnore( - "my-project/.gitignore", - ` - node_modules - .wrangler/ # This is for wrangler - .vscode - ` - ); - addWranglerToGitIgnore({ - project: { path: "my-project" }, - } as unknown as C3Context); - - expect(appendFileMock.mock.calls).toMatchInlineSnapshot(` - [ - [ - "my-project/.gitignore", - " - .dev.vars* - !.dev.vars.example - .env* - !.env.example - ", - ], - ] - `); - }); - - function mockGitIgnore(path: string, content: string) { - vi.mocked(existsSync).mockImplementation( - (filePath: PathLike) => filePath === path - ); - vi.mocked(readFile).mockImplementation((filePath: string) => - filePath === path ? content.replace(/\n\s*/g, "\n") : "" - ); - } -}); describe("downloadRemoteTemplate", () => { let cloneMock: Mock; diff --git a/packages/create-cloudflare/src/cli.ts b/packages/create-cloudflare/src/cli.ts index 132c0d97df..8492645cd4 100644 --- a/packages/create-cloudflare/src/cli.ts +++ b/packages/create-cloudflare/src/cli.ts @@ -12,6 +12,7 @@ import { } from "@cloudflare/cli"; import { runCommand } from "@cloudflare/cli/command"; import { CancelError } from "@cloudflare/cli/error"; +import { maybeAppendWranglerToGitIgnore } from "@cloudflare/cli/gitignore"; import { isInteractive } from "@cloudflare/cli/interactive"; import { cliDefinition, parseArgs, processArgument } from "helpers/args"; import { C3_DEFAULTS, isUpdateAvailable } from "helpers/cli"; @@ -29,7 +30,6 @@ import { showHelp } from "./help"; import { reporter, runTelemetryCommand } from "./metrics"; import { createProject } from "./pages"; import { - addWranglerToGitIgnore, copyTemplateFiles, createContext, updatePackageName, @@ -166,7 +166,9 @@ const create = async (ctx: C3Context) => { const configure = async (ctx: C3Context) => { startSection( - `Configuring your application for Cloudflare${ctx.args.experimental ? ` via \`wrangler setup\`` : ""}`, + `Configuring your application for Cloudflare${ + ctx.args.experimental ? ` via \`wrangler setup\`` : "" + }`, "Step 2 of 3" ); @@ -195,7 +197,7 @@ const configure = async (ctx: C3Context) => { await template.configure({ ...ctx }); } - addWranglerToGitIgnore(ctx); + maybeAppendWranglerToGitIgnore(ctx.project.path); await updatePackageScripts(ctx); diff --git a/packages/create-cloudflare/src/templates.ts b/packages/create-cloudflare/src/templates.ts index a11577152b..5a9eb8b334 100644 --- a/packages/create-cloudflare/src/templates.ts +++ b/packages/create-cloudflare/src/templates.ts @@ -10,8 +10,6 @@ import degit from "degit"; import { processArgument } from "helpers/args"; import { C3_DEFAULTS } from "helpers/cli"; import { - appendFile, - directoryExists, hasTsConfig, readFile, readJSON, @@ -937,7 +935,9 @@ export async function downloadRemoteTemplate( pathSegments.splice(0, 2); // Remove 'tree' and branch name } - src = `github:${user}/${repo}${pathSegments.length > 0 ? `/${pathSegments.join("/")}` : ""}${branch ? `#${branch}` : ""}`; + src = `github:${user}/${repo}${ + pathSegments.length > 0 ? `/${pathSegments.join("/")}` : "" + }${branch ? `#${branch}` : ""}`; } } @@ -1055,102 +1055,3 @@ export const getCopyFilesDestinationDir = ( return copyFiles.destinationDir(ctx); }; - -export const addWranglerToGitIgnore = (ctx: C3Context) => { - const gitIgnorePath = `${ctx.project.path}/.gitignore`; - const gitIgnorePreExisted = existsSync(gitIgnorePath); - - const gitDirExists = directoryExists(`${ctx.project.path}/.git`); - - if (!gitIgnorePreExisted && !gitDirExists) { - // if there is no .gitignore file and neither a .git directory - // then bail as the project is likely not targeting/using git - return; - } - - if (!gitIgnorePreExisted) { - writeFile(gitIgnorePath, ""); - } - - const existingGitIgnoreContent = readFile(gitIgnorePath); - const wranglerGitIgnoreFilesToAdd: string[] = []; - - const hasDotWrangler = existingGitIgnoreContent.match( - /^\/?\.wrangler(\/|\s|$)/m - ); - if (!hasDotWrangler) { - wranglerGitIgnoreFilesToAdd.push(".wrangler"); - } - - const hasDotDevDotVars = existingGitIgnoreContent.match( - /^\/?\.dev\.vars\*(\s|$)/m - ); - if (!hasDotDevDotVars) { - wranglerGitIgnoreFilesToAdd.push(".dev.vars*"); - } - - const hasDotDevVarsExample = existingGitIgnoreContent.match( - /^!\/?\.dev\.vars\.example(\s|$)/m - ); - if (!hasDotDevVarsExample) { - wranglerGitIgnoreFilesToAdd.push("!.dev.vars.example"); - } - - /** - * We check for the following type of occurrences: - * - * ``` - * .env - * .env* - * .env. - * .env*. - * ``` - * - * Any of these may alone on a line or be followed by a space and a trailing comment: - * - * ``` - * .env. # some trailing comment - * ``` - */ - const hasDotEnv = existingGitIgnoreContent.match( - /^\/?\.env\*?(\..*?)?(\s|$)/m - ); - if (!hasDotEnv) { - wranglerGitIgnoreFilesToAdd.push(".env*"); - } - - const hasDotEnvExample = existingGitIgnoreContent.match( - /^!\/?\.env\.example(\s|$)/m - ); - if (!hasDotEnvExample) { - wranglerGitIgnoreFilesToAdd.push("!.env.example"); - } - - if (wranglerGitIgnoreFilesToAdd.length === 0) { - return; - } - - const s = spinner(); - s.start("Adding Wrangler files to the .gitignore file"); - - const linesToAppend = [ - "", - ...(!existingGitIgnoreContent.match(/\n\s*$/) ? [""] : []), - ]; - - if (!hasDotWrangler && wranglerGitIgnoreFilesToAdd.length > 1) { - linesToAppend.push("# wrangler files"); - } - - wranglerGitIgnoreFilesToAdd.forEach((line) => linesToAppend.push(line)); - - linesToAppend.push(""); - - appendFile(gitIgnorePath, linesToAppend.join("\n")); - - s.stop( - `${brandColor(gitIgnorePreExisted ? "updated" : "created")} ${dim( - ".gitignore file" - )}` - ); -}; diff --git a/packages/wrangler/src/autoconfig/add-wrangler-assetsignore.ts b/packages/wrangler/src/autoconfig/add-wrangler-assetsignore.ts deleted file mode 100644 index ebd82f9513..0000000000 --- a/packages/wrangler/src/autoconfig/add-wrangler-assetsignore.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { appendFileSync, existsSync, writeFileSync } from "node:fs"; -import { brandColor, dim } from "@cloudflare/cli/colors"; -import { spinner } from "@cloudflare/cli/interactive"; -import { readFileSync } from "@cloudflare/workers-utils"; - -export const addWranglerToAssetsIgnore = (projectPath: string) => { - const assetsIgnorePath = `${projectPath}/.assetsignore`; - const assetsIgnorePreExisted = existsSync(assetsIgnorePath); - - if (!assetsIgnorePreExisted) { - writeFileSync(assetsIgnorePath, ""); - } - - const existingAssetsIgnoreContent = readFileSync(assetsIgnorePath); - const wranglerAssetsIgnoreFilesToAdd: string[] = []; - - const hasDotWrangler = existingAssetsIgnoreContent.match( - /^\/?\.wrangler(\/|\s|$)/m - ); - if (!hasDotWrangler) { - wranglerAssetsIgnoreFilesToAdd.push(".wrangler"); - } - - const hasDotDevDotVars = existingAssetsIgnoreContent.match( - /^\/?\.dev\.vars\*(\s|$)/m - ); - if (!hasDotDevDotVars) { - wranglerAssetsIgnoreFilesToAdd.push(".dev.vars*"); - } - - const hasDotDevVarsExample = existingAssetsIgnoreContent.match( - /^!\/?\.dev\.vars\.example(\s|$)/m - ); - if (!hasDotDevVarsExample) { - wranglerAssetsIgnoreFilesToAdd.push("!.dev.vars.example"); - } - - /** - * We check for the following type of occurrences: - * - * ``` - * .env - * .env* - * .env. - * .env*. - * ``` - * - * Any of these may alone on a line or be followed by a space and a trailing comment: - * - * ``` - * .env. # some trailing comment - * ``` - */ - const hasDotEnv = existingAssetsIgnoreContent.match( - /^\/?\.env\*?(\..*?)?(\s|$)/m - ); - if (!hasDotEnv) { - wranglerAssetsIgnoreFilesToAdd.push(".env*"); - } - - const hasDotEnvExample = existingAssetsIgnoreContent.match( - /^!\/?\.env\.example(\s|$)/m - ); - if (!hasDotEnvExample) { - wranglerAssetsIgnoreFilesToAdd.push("!.env.example"); - } - - if (wranglerAssetsIgnoreFilesToAdd.length === 0) { - return; - } - - const s = spinner(); - s.start("Adding Wrangler files to the .assetsignore file"); - - const linesToAppend = [ - ...(assetsIgnorePreExisted - ? ["", ...(!existingAssetsIgnoreContent.match(/\n\s*$/) ? [""] : [])] - : []), - ]; - - if (!hasDotWrangler && wranglerAssetsIgnoreFilesToAdd.length > 1) { - linesToAppend.push("# wrangler files"); - } - - wranglerAssetsIgnoreFilesToAdd.forEach((line) => linesToAppend.push(line)); - - linesToAppend.push(""); - - appendFileSync(assetsIgnorePath, linesToAppend.join("\n")); - - s.stop( - `${brandColor(assetsIgnorePreExisted ? "updated" : "created")} ${dim( - ".assetsignore file" - )}` - ); -}; diff --git a/packages/wrangler/src/autoconfig/git.ts b/packages/wrangler/src/autoconfig/git.ts deleted file mode 100644 index 7a4521f1ae..0000000000 --- a/packages/wrangler/src/autoconfig/git.ts +++ /dev/null @@ -1,48 +0,0 @@ -import assert from "node:assert"; -import { existsSync, statSync } from "node:fs"; -import { appendFile, writeFile } from "node:fs/promises"; -import { spinner } from "@cloudflare/cli/interactive"; - -// TODO: the logic in this file is partially duplicated with the logic in packages/wrangler/src/autoconfig/c3-vendor/add-wrangler-gitignore.ts -// and also in packages/wrangler/src/autoconfig/add-wrangler-assetsignore.ts, when we get rid of the c3-vendor directory -// we should clean this duplication up - -function directoryExists(path: string): boolean { - const stat = statSync(path, { throwIfNoEntry: false }); - return stat?.isDirectory() ?? false; -} - -export async function appendToGitIgnore( - projectPath: string, - textToAppend: string, - spinnerOptions?: { startText: string; doneText: string } -) { - const gitIgnorePath = `${projectPath}/.gitignore`; - const gitIgnorePreExisted = existsSync(gitIgnorePath); - - const gitDirExists = directoryExists(`${projectPath}/.git`); - - if (!gitIgnorePreExisted && !gitDirExists) { - // if there is no .gitignore file and neither a .git directory - // then bail as the project is likely not targeting/using git - return; - } - - const s = spinnerOptions ? spinner() : null; - - if (spinnerOptions) { - assert(s); - s.start(spinnerOptions.startText); - } - - if (!gitIgnorePreExisted) { - await writeFile(gitIgnorePath, ""); - } - - await appendFile(gitIgnorePath, textToAppend); - - if (spinnerOptions) { - assert(s); - s.stop(spinnerOptions.doneText); - } -} diff --git a/packages/wrangler/src/autoconfig/run.ts b/packages/wrangler/src/autoconfig/run.ts index dfc3a583be..83cc001b68 100644 --- a/packages/wrangler/src/autoconfig/run.ts +++ b/packages/wrangler/src/autoconfig/run.ts @@ -2,6 +2,10 @@ import assert from "node:assert"; import { existsSync } from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; +import { + maybeAppendWranglerToGitIgnoreLikeFile, + maybeAppendWranglerToGitIgnore, +} from "@cloudflare/cli/gitignore"; import { installWrangler } from "@cloudflare/cli/packages"; import { FatalError, @@ -13,8 +17,6 @@ import { confirm } from "../dialogs"; import { logger } from "../logger"; import { sendMetricsEvent } from "../metrics"; import { sanitizeError } from "../metrics/sanitization"; -import { addWranglerToAssetsIgnore } from "./add-wrangler-assetsignore"; -import { addWranglerToGitIgnore } from "./c3-vendor/add-wrangler-gitignore"; import { assertNonConfigured, confirmAutoConfigDetails, @@ -239,11 +241,13 @@ export async function runAutoConfig( ); } - addWranglerToGitIgnore(autoConfigDetails.projectPath); + maybeAppendWranglerToGitIgnore(autoConfigDetails.projectPath); // If we're uploading the project path as the output directory, make sure we don't accidentally upload any sensitive Wrangler files if (autoConfigDetails.outputDir === autoConfigDetails.projectPath) { - addWranglerToAssetsIgnore(autoConfigDetails.projectPath); + maybeAppendWranglerToGitIgnoreLikeFile( + `${autoConfigDetails.projectPath}/.assetsignore` + ); } const buildCommand =