Skip to content

Commit 88d76b8

Browse files
committed
feat(create): always exclude node_modules from .gitignore after git init
Bundled `@org` templates may ship without a `.gitignore` (or with one that omits `node_modules`), so the user's first commit could end up tracking installed dependencies. Add a small post-`git init` step that guarantees the line is present. - `create/utils.ts` — new `ensureGitignoreNodeModules(projectDir)` that: - creates a fresh `.gitignore` containing `node_modules\n` when none exists (the bundled-monorepo case), - appends `node_modules\n` when a `.gitignore` exists but omits it (terminating the previous line first if the file lacks a trailing newline), - is a no-op when `node_modules` (or `node_modules/`) already appears as a standalone line. - `create/bin.ts` — call the helper from the monorepo branch right after `git init` succeeds. Both the builtin `vite:monorepo` (whose template's `_gitignore` already lists `node_modules`) and the bundled `@org` monorepo path now end up with the line guaranteed. Tests: - 5 unit tests over the helper (fresh, append, no-newline, no-op, trailing-slash variant). - The bundled-monorepo snap-test gains a `cat my-mono/.gitignore` step verifying the file is created with `node_modules`. - Existing `new-vite-monorepo` snap-test stays byte-stable — the template-shipped gitignore already lists `node_modules`, so the helper is a no-op there.
1 parent 8f7095f commit 88d76b8

5 files changed

Lines changed: 83 additions & 3 deletions

File tree

packages/cli/snap-tests/create-org-bundled-monorepo/snap.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ peerDependencyRules:
3737

3838
> test -d my-mono/.git && echo 'Git initialized' || echo 'No git' # git-init prompt covers bundled monorepo path
3939
Git initialized
40+
41+
> cat my-mono/.gitignore # node_modules excluded even though tarball shipped no .gitignore
42+
node_modules

packages/cli/snap-tests/create-org-bundled-monorepo/steps.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"node $SNAP_CASES_DIR/.shared/mock-npm-registry.mjs -- vp create @your-org:workspace --no-interactive --directory my-mono # bundled monorepo: extract tarball, scaffold, inject create.defaultTemplate",
44
"cat my-mono/vite.config.ts # create.defaultTemplate auto-set to @your-org",
55
"cat my-mono/pnpm-workspace.yaml # workspace markers preserved",
6-
"test -d my-mono/.git && echo 'Git initialized' || echo 'No git' # git-init prompt covers bundled monorepo path"
6+
"test -d my-mono/.git && echo 'Git initialized' || echo 'No git' # git-init prompt covers bundled monorepo path",
7+
"cat my-mono/.gitignore # node_modules excluded even though tarball shipped no .gitignore"
78
]
89
}

packages/cli/src/create/__tests__/utils.spec.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import { describe, expect, it } from 'vitest';
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
5+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
26

37
import {
48
deriveDefaultPackageName,
9+
ensureGitignoreNodeModules,
510
formatTargetDir,
611
getProjectDirFromPackageName,
712
} from '../utils.js';
@@ -87,3 +92,50 @@ describe('deriveDefaultPackageName', () => {
8792
expect(result.length).toBeGreaterThan(0);
8893
});
8994
});
95+
96+
describe('ensureGitignoreNodeModules', () => {
97+
let projectDir: string;
98+
99+
beforeEach(() => {
100+
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-gitignore-'));
101+
});
102+
103+
afterEach(() => {
104+
fs.rmSync(projectDir, { recursive: true, force: true });
105+
});
106+
107+
function gitignore(): string {
108+
return fs.readFileSync(path.join(projectDir, '.gitignore'), 'utf-8');
109+
}
110+
111+
it('creates a fresh `.gitignore` with `node_modules` when none exists', () => {
112+
ensureGitignoreNodeModules(projectDir);
113+
expect(gitignore()).toBe('node_modules\n');
114+
});
115+
116+
it('appends `node_modules` to an existing `.gitignore` that omits it', () => {
117+
fs.writeFileSync(path.join(projectDir, '.gitignore'), 'dist\n*.log\n');
118+
ensureGitignoreNodeModules(projectDir);
119+
expect(gitignore()).toBe('dist\n*.log\nnode_modules\n');
120+
});
121+
122+
it('terminates the last line first when the existing file lacks a trailing newline', () => {
123+
fs.writeFileSync(path.join(projectDir, '.gitignore'), 'dist');
124+
ensureGitignoreNodeModules(projectDir);
125+
expect(gitignore()).toBe('dist\nnode_modules\n');
126+
});
127+
128+
it('is a no-op when `node_modules` already appears as a standalone line', () => {
129+
const existing = '# Logs\n*.log\nnode_modules\ndist\n';
130+
fs.writeFileSync(path.join(projectDir, '.gitignore'), existing);
131+
ensureGitignoreNodeModules(projectDir);
132+
expect(gitignore()).toBe(existing);
133+
});
134+
135+
it('treats `node_modules/` (with trailing slash) as a match', () => {
136+
const existing = 'node_modules/\ndist\n';
137+
fs.writeFileSync(path.join(projectDir, '.gitignore'), existing);
138+
ensureGitignoreNodeModules(projectDir);
139+
expect(gitignore()).toBe(existing);
140+
});
141+
});

packages/cli/src/create/bin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ import {
6565
executeRemoteTemplate,
6666
} from './templates/index.ts';
6767
import { BuiltinTemplate, TemplateType } from './templates/types.ts';
68-
import { deriveDefaultPackageName, formatTargetDir } from './utils.ts';
68+
import { deriveDefaultPackageName, ensureGitignoreNodeModules, formatTargetDir } from './utils.ts';
6969

7070
const helpMessage = renderCliDoc({
7171
usage: 'vp create [TEMPLATE] [OPTIONS] [-- TEMPLATE_OPTIONS]',
@@ -873,6 +873,7 @@ Use \`vp create --list\` to list all available templates, or run \`vp create --h
873873
if (!compactOutput) {
874874
prompts.log.success('Git repository initialized');
875875
}
876+
ensureGitignoreNodeModules(fullPath);
876877
} else {
877878
prompts.log.warn('Failed to initialize git repository');
878879
if (gitResult.stderr) {

packages/cli/src/create/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,29 @@ export function setPackageName(projectDir: string, packageName: string) {
112112
});
113113
}
114114

115+
/**
116+
* Make sure the scaffolded project's `.gitignore` excludes `node_modules`.
117+
*
118+
* Called right after `git init` so even bundled `@org` templates (which
119+
* may ship without a `.gitignore`) don't end up tracking installed
120+
* dependencies on the user's first commit. No-op when an existing
121+
* `.gitignore` already lists `node_modules`.
122+
*/
123+
export function ensureGitignoreNodeModules(projectDir: string): void {
124+
const gitignorePath = path.join(projectDir, '.gitignore');
125+
let content = '';
126+
try {
127+
content = fs.readFileSync(gitignorePath, 'utf-8');
128+
} catch {
129+
// No existing .gitignore — we'll write a fresh one below.
130+
}
131+
if (/^\s*node_modules\/?\s*$/m.test(content)) {
132+
return;
133+
}
134+
const prefix = content === '' || content.endsWith('\n') ? '' : '\n';
135+
fs.appendFileSync(gitignorePath, `${prefix}node_modules\n`);
136+
}
137+
115138
export function formatDisplayTargetDir(targetDir: string) {
116139
const normalized = targetDir.split(path.sep).join('/');
117140
if (normalized === '' || normalized === '.') {

0 commit comments

Comments
 (0)