Skip to content

Commit ccffc9b

Browse files
authored
feat(create): support underscore dotfile rename for @org/create bundled templates (#1574)
Closes #1570. ## Summary `vp create @org:<name>` for a bundled template (relative `./...` `template`) now renames a small set of underscore-prefixed files to their dotfile equivalents: | File in `@org/create` | File in scaffolded project | | --------------------- | -------------------------- | | `_gitignore` | `.gitignore` | | `_npmrc` | `.npmrc` | | `_yarnrc.yml` | `.yarnrc.yml` | This lets `@org/create` maintainers keep these files as plain text inside their own package without IDE/Git treating them as live config. The same convention is already used by `create-vite` and the built-in `vite:monorepo` template — this PR extracts the helper from `monorepo.ts` to `utils.ts` and applies it to the bundled-template path. Documented under "Bundled subdirectory templates" in the create guide.
1 parent 1ff644c commit ccffc9b

9 files changed

Lines changed: 127 additions & 18 deletions

File tree

docs/guide/create.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ An invalid manifest is a hard error, not a silent fall-through — a maintainer
185185

186186
### Bundled subdirectory templates
187187

188-
Relative `./...` paths resolve against the enclosing `@org/create` package root — **not** the user's cwd. The referenced directory is copied verbatim into the target project (no template-engine processing). Paths that escape the package root are rejected.
188+
Relative `./...` paths resolve against the enclosing `@org/create` package root — **not** the user's cwd. The referenced directory is copied into the target project as-is (no template-engine processing); the only exception is that a small set of underscore-prefixed scaffold files (`_gitignore`, `_npmrc`, `_yarnrc.yml`) are renamed to their dotfile equivalents. Paths that escape the package root are rejected.
189189

190190
### Make the org the default in a repo
191191

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"@your-org/create": {
3+
"name": "@your-org/create",
4+
"dist-tags": { "latest": "1.0.0" },
5+
"versions": {
6+
"1.0.0": {
7+
"version": "1.0.0",
8+
"dist": {
9+
"tarball": "{REGISTRY}/@your-org/create/-/create-1.0.0.tgz",
10+
"integrity": "sha512-e7obtbeDFpoRewJvBuspE70GOluDTs3tZ6N1sMTOGlSjphtT5sMH00OkerY9SFX8ESXixKPOIp5fJkHqdxLn1Q=="
11+
},
12+
"createConfig": {
13+
"templates": [
14+
{
15+
"name": "demo",
16+
"description": "Bundled demo template with dotfiles",
17+
"template": "./templates/demo"
18+
}
19+
]
20+
}
21+
}
22+
}
23+
}
24+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
> node $SNAP_CASES_DIR/.shared/mock-npm-registry.mjs -- vp create @your-org:demo --no-interactive --directory my-demo-app # bundled template with _gitignore/_npmrc
2+
◇ Scaffolded my-demo-app
3+
• Node <semver> pnpm <semver>
4+
→ Next: cd my-demo-app && vp run
5+
6+
> ls -A my-demo-app # verify _gitignore/_npmrc were renamed and no underscore variants remain
7+
.gitignore
8+
.npmrc
9+
.vite-hooks
10+
AGENTS.md
11+
package.json
12+
pnpm-workspace.yaml
13+
src
14+
vite.config.ts
15+
16+
> cat my-demo-app/.gitignore # verify _gitignore content was preserved
17+
node_modules
18+
dist
19+
20+
> cat my-demo-app/.npmrc # verify _npmrc content was preserved
21+
auto-install-peers=true
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"commands": [
3+
"node $SNAP_CASES_DIR/.shared/mock-npm-registry.mjs -- vp create @your-org:demo --no-interactive --directory my-demo-app # bundled template with _gitignore/_npmrc",
4+
"ls -A my-demo-app # verify _gitignore/_npmrc were renamed and no underscore variants remain",
5+
"cat my-demo-app/.gitignore # verify _gitignore content was preserved",
6+
"cat my-demo-app/.npmrc # verify _npmrc content was preserved"
7+
]
8+
}
Binary file not shown.

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ensureGitignoreNodeModules,
1010
formatTargetDir,
1111
getProjectDirFromPackageName,
12+
renameFiles,
1213
} from '../utils.js';
1314

1415
describe('getProjectDirFromPackageName', () => {
@@ -158,3 +159,55 @@ describe('ensureGitignoreNodeModules', () => {
158159
expect(gitignore()).toBe('!node_modules\nnode_modules\n');
159160
});
160161
});
162+
163+
describe('renameFiles', () => {
164+
let projectDir: string;
165+
166+
beforeEach(() => {
167+
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-rename-'));
168+
});
169+
170+
afterEach(() => {
171+
fs.rmSync(projectDir, { recursive: true, force: true });
172+
});
173+
174+
function write(name: string, content: string): void {
175+
fs.writeFileSync(path.join(projectDir, name), content);
176+
}
177+
178+
function read(name: string): string {
179+
return fs.readFileSync(path.join(projectDir, name), 'utf-8');
180+
}
181+
182+
function exists(name: string): boolean {
183+
return fs.existsSync(path.join(projectDir, name));
184+
}
185+
186+
it('renames `_gitignore` to `.gitignore`', () => {
187+
write('_gitignore', 'node_modules\n');
188+
renameFiles(projectDir);
189+
expect(exists('_gitignore')).toBe(false);
190+
expect(read('.gitignore')).toBe('node_modules\n');
191+
});
192+
193+
it('renames `_npmrc` and `_yarnrc.yml`', () => {
194+
write('_npmrc', 'auto-install-peers=true\n');
195+
write('_yarnrc.yml', 'nodeLinker: node-modules\n');
196+
renameFiles(projectDir);
197+
expect(exists('_npmrc')).toBe(false);
198+
expect(exists('_yarnrc.yml')).toBe(false);
199+
expect(read('.npmrc')).toBe('auto-install-peers=true\n');
200+
expect(read('.yarnrc.yml')).toBe('nodeLinker: node-modules\n');
201+
});
202+
203+
it('is a no-op when no source files exist', () => {
204+
expect(() => renameFiles(projectDir)).not.toThrow();
205+
expect(fs.readdirSync(projectDir)).toEqual([]);
206+
});
207+
208+
it('leaves unmapped underscore files untouched', () => {
209+
write('_foo', 'bar\n');
210+
renameFiles(projectDir);
211+
expect(read('_foo')).toBe('bar\n');
212+
});
213+
});

packages/cli/src/create/templates/bundled.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'node:path';
33

44
import type { WorkspaceInfo } from '../../types/index.ts';
55
import type { ExecutionWithProjectDir } from '../command.ts';
6-
import { copyDir, setPackageName } from '../utils.ts';
6+
import { copyDir, renameFiles, setPackageName } from '../utils.ts';
77
import type { BuiltinTemplateInfo } from './types.ts';
88

99
/**
@@ -30,6 +30,8 @@ export async function executeBundledTemplate(
3030
throw error;
3131
}
3232

33+
renameFiles(destDir);
34+
3335
try {
3436
setPackageName(destDir, templateInfo.packageName);
3537
} catch {

packages/cli/src/create/templates/monorepo.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { editJsonFile } from '../../utils/json.ts';
1010
import { templatesDir } from '../../utils/path.ts';
1111
import type { ExecutionWithProjectDir } from '../command.ts';
1212
import { discoverTemplate } from '../discovery.ts';
13-
import { copyDir, formatDisplayTargetDir, setPackageName } from '../utils.ts';
13+
import { copyDir, formatDisplayTargetDir, renameFiles, setPackageName } from '../utils.ts';
1414
import { runRemoteTemplateCommand } from './remote.ts';
1515
import { type BuiltinTemplateInfo, LibraryTemplateRepo } from './types.ts';
1616

@@ -158,21 +158,6 @@ export async function executeMonorepoTemplate(
158158
return { exitCode: 0, projectDir: templateInfo.targetDir };
159159
}
160160

161-
const RENAME_FILES: Record<string, string> = {
162-
_gitignore: '.gitignore',
163-
_npmrc: '.npmrc',
164-
'_yarnrc.yml': '.yarnrc.yml',
165-
};
166-
167-
function renameFiles(projectDir: string) {
168-
for (const [from, to] of Object.entries(RENAME_FILES)) {
169-
const fromPath = path.join(projectDir, from);
170-
if (fs.existsSync(fromPath)) {
171-
fs.renameSync(fromPath, path.join(projectDir, to));
172-
}
173-
}
174-
}
175-
176161
function getScopeFromPackageName(packageName: string) {
177162
if (packageName.startsWith('@')) {
178163
return packageName.split('/')[0];

packages/cli/src/create/utils.ts

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

115+
const RENAME_FILES = {
116+
_gitignore: '.gitignore',
117+
_npmrc: '.npmrc',
118+
'_yarnrc.yml': '.yarnrc.yml',
119+
} as const;
120+
121+
/** Rename underscore-prefixed scaffold files to their dotfile names in `projectDir`. */
122+
export function renameFiles(projectDir: string): void {
123+
for (const [from, to] of Object.entries(RENAME_FILES)) {
124+
const fromPath = path.join(projectDir, from);
125+
if (fs.existsSync(fromPath)) {
126+
fs.renameSync(fromPath, path.join(projectDir, to));
127+
}
128+
}
129+
}
130+
115131
/**
116132
* Make sure the scaffolded project's `.gitignore` excludes `node_modules`.
117133
*

0 commit comments

Comments
 (0)