diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/snapshots/create_org_bundled_monorepo.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/snapshots/create_org_bundled_monorepo.md index 56c6ff3bb4..393c40aaee 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/snapshots/create_org_bundled_monorepo.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/snapshots/create_org_bundled_monorepo.md @@ -66,4 +66,9 @@ node_modules excluded even though tarball shipped no .gitignore ``` node_modules + +# dotenv environment variable files +.env +.env.* +!.env.example ``` diff --git a/packages/cli/src/create/__tests__/utils.spec.ts b/packages/cli/src/create/__tests__/utils.spec.ts index ecec51f39c..4217b4b216 100644 --- a/packages/cli/src/create/__tests__/utils.spec.ts +++ b/packages/cli/src/create/__tests__/utils.spec.ts @@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { deriveDefaultPackageName, - ensureGitignoreNodeModules, + ensureDefaultGitignoreEntries, ensureGitignoreVsCodeEditorConfigs, formatTargetDir, getProjectDirFromPackageName, @@ -124,7 +124,7 @@ describe('deriveDefaultPackageName', () => { }); }); -describe('ensureGitignoreNodeModules', () => { +describe('ensureDefaultGitignoreEntries', () => { let projectDir: string; beforeEach(() => { @@ -139,54 +139,67 @@ describe('ensureGitignoreNodeModules', () => { return fs.readFileSync(path.join(projectDir, '.gitignore'), 'utf-8'); } - it('creates a fresh `.gitignore` with `node_modules` when none exists', () => { - ensureGitignoreNodeModules(projectDir); - expect(gitignore()).toBe('node_modules\n'); + const dotenvBlock = '# dotenv environment variable files\n.env\n.env.*\n!.env.example\n'; + + it('creates a fresh `.gitignore` with default entries when none exists', () => { + ensureDefaultGitignoreEntries(projectDir); + expect(gitignore()).toBe(`node_modules\n\n${dotenvBlock}`); }); - it('appends `node_modules` to an existing `.gitignore` that omits it', () => { + it('appends default entries to an existing `.gitignore` that omits them', () => { fs.writeFileSync(path.join(projectDir, '.gitignore'), 'dist\n*.log\n'); - ensureGitignoreNodeModules(projectDir); - expect(gitignore()).toBe('dist\n*.log\nnode_modules\n'); + ensureDefaultGitignoreEntries(projectDir); + expect(gitignore()).toBe(`dist\n*.log\nnode_modules\n\n${dotenvBlock}`); }); it('terminates the last line first when the existing file lacks a trailing newline', () => { fs.writeFileSync(path.join(projectDir, '.gitignore'), 'dist'); - ensureGitignoreNodeModules(projectDir); - expect(gitignore()).toBe('dist\nnode_modules\n'); + ensureDefaultGitignoreEntries(projectDir); + expect(gitignore()).toBe(`dist\nnode_modules\n\n${dotenvBlock}`); }); - it('is a no-op when `node_modules` already appears as a standalone line', () => { + it('appends dotenv entries when `node_modules` already appears as a standalone line', () => { const existing = '# Logs\n*.log\nnode_modules\ndist\n'; fs.writeFileSync(path.join(projectDir, '.gitignore'), existing); - ensureGitignoreNodeModules(projectDir); - expect(gitignore()).toBe(existing); + ensureDefaultGitignoreEntries(projectDir); + expect(gitignore()).toBe(`${existing}\n${dotenvBlock}`); }); it('treats `node_modules/` (with trailing slash) as a match', () => { const existing = 'node_modules/\ndist\n'; fs.writeFileSync(path.join(projectDir, '.gitignore'), existing); - ensureGitignoreNodeModules(projectDir); - expect(gitignore()).toBe(existing); + ensureDefaultGitignoreEntries(projectDir); + expect(gitignore()).toBe(`${existing}\n${dotenvBlock}`); }); - it('handles CRLF line endings without re-appending', () => { - const existing = 'node_modules\r\ndist\r\n'; + it('handles CRLF line endings without re-appending existing defaults', () => { + const existing = `node_modules\r\ndist\r\n${dotenvBlock.replaceAll('\n', '\r\n')}`; fs.writeFileSync(path.join(projectDir, '.gitignore'), existing); - ensureGitignoreNodeModules(projectDir); + ensureDefaultGitignoreEntries(projectDir); expect(gitignore()).toBe(existing); }); it('does not consider a `node_modules/sub` subpath as already excluded', () => { fs.writeFileSync(path.join(projectDir, '.gitignore'), 'node_modules/sub\n'); - ensureGitignoreNodeModules(projectDir); - expect(gitignore()).toBe('node_modules/sub\nnode_modules\n'); + ensureDefaultGitignoreEntries(projectDir); + expect(gitignore()).toBe(`node_modules/sub\nnode_modules\n\n${dotenvBlock}`); }); it('does not match `!node_modules` (an explicit un-ignore override)', () => { fs.writeFileSync(path.join(projectDir, '.gitignore'), '!node_modules\n'); - ensureGitignoreNodeModules(projectDir); - expect(gitignore()).toBe('!node_modules\nnode_modules\n'); + ensureDefaultGitignoreEntries(projectDir); + expect(gitignore()).toBe(`!node_modules\nnode_modules\n\n${dotenvBlock}`); + }); + + it('adds only missing dotenv entries', () => { + fs.writeFileSync( + path.join(projectDir, '.gitignore'), + '# dotenv environment variable files\n.env\nnode_modules\n', + ); + ensureDefaultGitignoreEntries(projectDir); + expect(gitignore()).toBe( + '# dotenv environment variable files\n.env\nnode_modules\n.env.*\n!.env.example\n', + ); }); }); diff --git a/packages/cli/src/create/bin.ts b/packages/cli/src/create/bin.ts index 246dc2f500..0ef9c028e1 100644 --- a/packages/cli/src/create/bin.ts +++ b/packages/cli/src/create/bin.ts @@ -84,7 +84,7 @@ import { import { BuiltinTemplate, TemplateType } from './templates/types.ts'; import { deriveDefaultPackageName, - ensureGitignoreNodeModules, + ensureDefaultGitignoreEntries, ensureGitignoreVsCodeEditorConfigs, formatTargetDir, normalizeEditorOption, @@ -1073,7 +1073,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h if (!compactOutput) { prompts.log.success('Git repository initialized'); } - ensureGitignoreNodeModules(fullPath); + ensureDefaultGitignoreEntries(fullPath); } else { prompts.log.warn('Failed to initialize git repository'); if (gitResult.stderr) { @@ -1111,7 +1111,9 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h rewriteMonorepo(workspaceInfo, undefined, compactOutput); if (shouldSetupGit) { updateCreateProgress('Initializing git repository'); - await initGitRepository(fullPath); + if (await initGitRepository(fullPath)) { + ensureDefaultGitignoreEntries(fullPath); + } } if (bundled?.monorepo) { // Wire `create.defaultTemplate: ''` into the new workspace's @@ -1434,7 +1436,9 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h } if (shouldSetupGit) { updateCreateProgress('Initializing git repository'); - await initGitRepository(fullPath); + if (await initGitRepository(fullPath)) { + ensureDefaultGitignoreEntries(fullPath); + } } if (shouldSetupHooks) { installGitHooks(fullPath, compactOutput, undefined, workspaceInfo.packageManager); diff --git a/packages/cli/src/create/utils.ts b/packages/cli/src/create/utils.ts index 6ae984e8e5..3dc77c3081 100644 --- a/packages/cli/src/create/utils.ts +++ b/packages/cli/src/create/utils.ts @@ -161,15 +161,22 @@ export function renameFiles(projectDir: string): void { } } +const DOTENV_GITIGNORE_LINES = [ + '# dotenv environment variable files', + '.env', + '.env.*', + '!.env.example', +] as const; + /** - * Make sure the scaffolded project's `.gitignore` excludes `node_modules`. + * Make sure the scaffolded project's `.gitignore` excludes default generated + * project artifacts. * * Called right after `git init` so even bundled `@org` templates (which - * may ship without a `.gitignore`) don't end up tracking installed - * dependencies on the user's first commit. No-op when an existing - * `.gitignore` already lists `node_modules`. + * may ship without a `.gitignore`) don't end up tracking dependencies or + * local environment files on the user's first commit. */ -export function ensureGitignoreNodeModules(projectDir: string): void { +export function ensureDefaultGitignoreEntries(projectDir: string): void { const gitignorePath = path.join(projectDir, '.gitignore'); let content = ''; try { @@ -177,11 +184,44 @@ export function ensureGitignoreNodeModules(projectDir: string): void { } catch { // No existing .gitignore — we'll write a fresh one below. } - if (/^\s*node_modules\/?\s*$/m.test(content)) { + + const lines: string[] = []; + if (!hasNodeModulesGitignoreLine(content)) { + lines.push('node_modules'); + } + + const missingDotenvLines = DOTENV_GITIGNORE_LINES.filter( + (line) => !hasGitignoreLine(content, line), + ); + if (missingDotenvLines.length > 0) { + const startsDotenvSection = missingDotenvLines[0] === DOTENV_GITIGNORE_LINES[0]; + if (lines.length > 0 || (startsDotenvSection && content.trim() !== '')) { + lines.push(''); + } + lines.push(...missingDotenvLines); + } + + appendGitignoreLines(gitignorePath, content, lines); +} + +function hasNodeModulesGitignoreLine(content: string): boolean { + return /^\s*node_modules\/?\s*$/m.test(content); +} + +function hasGitignoreLine(content: string, line: string): boolean { + return content.split(/\r?\n/).some((entry) => entry.trim() === line); +} + +function appendGitignoreLines( + gitignorePath: string, + content: string, + lines: readonly string[], +): void { + if (lines.length === 0) { return; } const prefix = content === '' || content.endsWith('\n') ? '' : '\n'; - fs.appendFileSync(gitignorePath, `${prefix}node_modules\n`); + fs.appendFileSync(gitignorePath, `${prefix}${lines.join('\n')}\n`); } const VSCODE_SETTINGS_PATH = '.vscode/settings.json'; @@ -218,18 +258,6 @@ function appendGitignoreVsCodeEditorConfigsBlock(gitignorePath: string, content: appendGitignoreLines(gitignorePath, content, VSCODE_CONFIG_UNIGNORE_BLOCK); } -function appendGitignoreLines( - gitignorePath: string, - content: string, - lines: readonly string[], -): void { - if (lines.length === 0) { - return; - } - const prefix = content === '' || content.endsWith('\n') ? '' : '\n'; - fs.appendFileSync(gitignorePath, `${prefix}${lines.join('\n')}\n`); -} - export function formatDisplayTargetDir(targetDir: string) { const normalized = targetDir.split(path.sep).join('/'); if (normalized === '' || normalized === '.') { diff --git a/packages/cli/templates/monorepo/_gitignore b/packages/cli/templates/monorepo/_gitignore index 08cc724aa3..b7a2a0e2ec 100644 --- a/packages/cli/templates/monorepo/_gitignore +++ b/packages/cli/templates/monorepo/_gitignore @@ -12,6 +12,11 @@ dist dist-ssr *.local +# dotenv environment variable files +.env +.env.* +!.env.example + # Editor directories and files .vscode/* !.vscode/settings.json