From 762160fc5a04daaa00f29ca3a9d83116bb50a947 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= <gyoung3063413@naver.com>
Date: Thu, 14 May 2026 14:57:35 +0900
Subject: [PATCH 1/2] feat(create): rename underscore dotfiles in @org/create
 bundled templates
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Apply `_gitignore` → `.gitignore`, `_npmrc` → `.npmrc`,
`_yarnrc.yml` → `.yarnrc.yml` when scaffolding bundled
`@org/create` templates so org maintainers can keep these as
plain text inside their package without IDE/Git treating them
as live config.

The rename was already shipped for the built-in monorepo
template; extract the shared helper to `utils.ts` and apply it
to `bundled.ts` as well. Add unit tests for the helper and
update the user guide.
---
 docs/guide/create.md                          |  2 +-
 .../cli/src/create/__tests__/utils.spec.ts    | 53 +++++++++++++++++++
 packages/cli/src/create/templates/bundled.ts  |  4 +-
 packages/cli/src/create/templates/monorepo.ts | 17 +-----
 packages/cli/src/create/utils.ts              | 16 ++++++
 5 files changed, 74 insertions(+), 18 deletions(-)

diff --git a/docs/guide/create.md b/docs/guide/create.md
index d9ff2c6d91..200be46aaa 100644
--- a/docs/guide/create.md
+++ b/docs/guide/create.md
@@ -185,7 +185,7 @@ An invalid manifest is a hard error, not a silent fall-through — a maintainer
 
 ### Bundled subdirectory templates
 
-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.
+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.
 
 ### Make the org the default in a repo
 
diff --git a/packages/cli/src/create/__tests__/utils.spec.ts b/packages/cli/src/create/__tests__/utils.spec.ts
index c2618b3be1..23d81941f1 100644
--- a/packages/cli/src/create/__tests__/utils.spec.ts
+++ b/packages/cli/src/create/__tests__/utils.spec.ts
@@ -9,6 +9,7 @@ import {
   ensureGitignoreNodeModules,
   formatTargetDir,
   getProjectDirFromPackageName,
+  renameFiles,
 } from '../utils.js';
 
 describe('getProjectDirFromPackageName', () => {
@@ -158,3 +159,55 @@ describe('ensureGitignoreNodeModules', () => {
     expect(gitignore()).toBe('!node_modules\nnode_modules\n');
   });
 });
+
+describe('renameFiles', () => {
+  let projectDir: string;
+
+  beforeEach(() => {
+    projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-rename-'));
+  });
+
+  afterEach(() => {
+    fs.rmSync(projectDir, { recursive: true, force: true });
+  });
+
+  function write(name: string, content: string): void {
+    fs.writeFileSync(path.join(projectDir, name), content);
+  }
+
+  function read(name: string): string {
+    return fs.readFileSync(path.join(projectDir, name), 'utf-8');
+  }
+
+  function exists(name: string): boolean {
+    return fs.existsSync(path.join(projectDir, name));
+  }
+
+  it('renames `_gitignore` to `.gitignore`', () => {
+    write('_gitignore', 'node_modules\n');
+    renameFiles(projectDir);
+    expect(exists('_gitignore')).toBe(false);
+    expect(read('.gitignore')).toBe('node_modules\n');
+  });
+
+  it('renames `_npmrc` and `_yarnrc.yml`', () => {
+    write('_npmrc', 'auto-install-peers=true\n');
+    write('_yarnrc.yml', 'nodeLinker: node-modules\n');
+    renameFiles(projectDir);
+    expect(exists('_npmrc')).toBe(false);
+    expect(exists('_yarnrc.yml')).toBe(false);
+    expect(read('.npmrc')).toBe('auto-install-peers=true\n');
+    expect(read('.yarnrc.yml')).toBe('nodeLinker: node-modules\n');
+  });
+
+  it('is a no-op when no source files exist', () => {
+    expect(() => renameFiles(projectDir)).not.toThrow();
+    expect(fs.readdirSync(projectDir)).toEqual([]);
+  });
+
+  it('leaves unmapped underscore files untouched', () => {
+    write('_foo', 'bar\n');
+    renameFiles(projectDir);
+    expect(read('_foo')).toBe('bar\n');
+  });
+});
diff --git a/packages/cli/src/create/templates/bundled.ts b/packages/cli/src/create/templates/bundled.ts
index 316f32dfb5..f85018f29e 100644
--- a/packages/cli/src/create/templates/bundled.ts
+++ b/packages/cli/src/create/templates/bundled.ts
@@ -3,7 +3,7 @@ import path from 'node:path';
 
 import type { WorkspaceInfo } from '../../types/index.ts';
 import type { ExecutionWithProjectDir } from '../command.ts';
-import { copyDir, setPackageName } from '../utils.ts';
+import { copyDir, renameFiles, setPackageName } from '../utils.ts';
 import type { BuiltinTemplateInfo } from './types.ts';
 
 /**
@@ -30,6 +30,8 @@ export async function executeBundledTemplate(
     throw error;
   }
 
+  renameFiles(destDir);
+
   try {
     setPackageName(destDir, templateInfo.packageName);
   } catch {
diff --git a/packages/cli/src/create/templates/monorepo.ts b/packages/cli/src/create/templates/monorepo.ts
index 3d72da0d47..03646972ef 100644
--- a/packages/cli/src/create/templates/monorepo.ts
+++ b/packages/cli/src/create/templates/monorepo.ts
@@ -10,7 +10,7 @@ import { editJsonFile } from '../../utils/json.ts';
 import { templatesDir } from '../../utils/path.ts';
 import type { ExecutionWithProjectDir } from '../command.ts';
 import { discoverTemplate } from '../discovery.ts';
-import { copyDir, formatDisplayTargetDir, setPackageName } from '../utils.ts';
+import { copyDir, formatDisplayTargetDir, renameFiles, setPackageName } from '../utils.ts';
 import { runRemoteTemplateCommand } from './remote.ts';
 import { type BuiltinTemplateInfo, LibraryTemplateRepo } from './types.ts';
 
@@ -158,21 +158,6 @@ export async function executeMonorepoTemplate(
   return { exitCode: 0, projectDir: templateInfo.targetDir };
 }
 
-const RENAME_FILES: Record<string, string> = {
-  _gitignore: '.gitignore',
-  _npmrc: '.npmrc',
-  '_yarnrc.yml': '.yarnrc.yml',
-};
-
-function renameFiles(projectDir: string) {
-  for (const [from, to] of Object.entries(RENAME_FILES)) {
-    const fromPath = path.join(projectDir, from);
-    if (fs.existsSync(fromPath)) {
-      fs.renameSync(fromPath, path.join(projectDir, to));
-    }
-  }
-}
-
 function getScopeFromPackageName(packageName: string) {
   if (packageName.startsWith('@')) {
     return packageName.split('/')[0];
diff --git a/packages/cli/src/create/utils.ts b/packages/cli/src/create/utils.ts
index 0f6fcea105..173336d923 100644
--- a/packages/cli/src/create/utils.ts
+++ b/packages/cli/src/create/utils.ts
@@ -112,6 +112,22 @@ export function setPackageName(projectDir: string, packageName: string) {
   });
 }
 
+const RENAME_FILES = {
+  _gitignore: '.gitignore',
+  _npmrc: '.npmrc',
+  '_yarnrc.yml': '.yarnrc.yml',
+} as const;
+
+/** Rename underscore-prefixed scaffold files to their dotfile names in `projectDir`. */
+export function renameFiles(projectDir: string): void {
+  for (const [from, to] of Object.entries(RENAME_FILES)) {
+    const fromPath = path.join(projectDir, from);
+    if (fs.existsSync(fromPath)) {
+      fs.renameSync(fromPath, path.join(projectDir, to));
+    }
+  }
+}
+
 /**
  * Make sure the scaffolded project's `.gitignore` excludes `node_modules`.
  *

From 273baacf19662150e620aae89e609adbed885b8c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= <gyoung3063413@naver.com>
Date: Thu, 14 May 2026 14:57:45 +0900
Subject: [PATCH 2/2] test(cli): add create-org-bundled-dotfiles snap-test

End-to-end coverage that `_gitignore` and `_npmrc` in a bundled
`@org/create` template tarball are renamed to their dotfile
equivalents in the scaffolded project.
---
 .../mock-manifest.json                        |  24 ++++++++++++++++++
 .../create-org-bundled-dotfiles/snap.txt      |  21 +++++++++++++++
 .../create-org-bundled-dotfiles/steps.json    |   8 ++++++
 .../tarballs/create-1.0.0.tgz                 | Bin 0 -> 496 bytes
 4 files changed, 53 insertions(+)
 create mode 100644 packages/cli/snap-tests/create-org-bundled-dotfiles/mock-manifest.json
 create mode 100644 packages/cli/snap-tests/create-org-bundled-dotfiles/snap.txt
 create mode 100644 packages/cli/snap-tests/create-org-bundled-dotfiles/steps.json
 create mode 100644 packages/cli/snap-tests/create-org-bundled-dotfiles/tarballs/create-1.0.0.tgz

diff --git a/packages/cli/snap-tests/create-org-bundled-dotfiles/mock-manifest.json b/packages/cli/snap-tests/create-org-bundled-dotfiles/mock-manifest.json
new file mode 100644
index 0000000000..fa33cc7366
--- /dev/null
+++ b/packages/cli/snap-tests/create-org-bundled-dotfiles/mock-manifest.json
@@ -0,0 +1,24 @@
+{
+  "@your-org/create": {
+    "name": "@your-org/create",
+    "dist-tags": { "latest": "1.0.0" },
+    "versions": {
+      "1.0.0": {
+        "version": "1.0.0",
+        "dist": {
+          "tarball": "{REGISTRY}/@your-org/create/-/create-1.0.0.tgz",
+          "integrity": "sha512-e7obtbeDFpoRewJvBuspE70GOluDTs3tZ6N1sMTOGlSjphtT5sMH00OkerY9SFX8ESXixKPOIp5fJkHqdxLn1Q=="
+        },
+        "createConfig": {
+          "templates": [
+            {
+              "name": "demo",
+              "description": "Bundled demo template with dotfiles",
+              "template": "./templates/demo"
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/packages/cli/snap-tests/create-org-bundled-dotfiles/snap.txt b/packages/cli/snap-tests/create-org-bundled-dotfiles/snap.txt
new file mode 100644
index 0000000000..2586a65241
--- /dev/null
+++ b/packages/cli/snap-tests/create-org-bundled-dotfiles/snap.txt
@@ -0,0 +1,21 @@
+> 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
+◇ Scaffolded my-demo-app
+• Node <semver>  pnpm <semver>
+→ Next: cd my-demo-app && vp run
+
+> ls -A my-demo-app # verify _gitignore/_npmrc were renamed and no underscore variants remain
+.gitignore
+.npmrc
+.vite-hooks
+AGENTS.md
+package.json
+pnpm-workspace.yaml
+src
+vite.config.ts
+
+> cat my-demo-app/.gitignore # verify _gitignore content was preserved
+node_modules
+dist
+
+> cat my-demo-app/.npmrc # verify _npmrc content was preserved
+auto-install-peers=true
diff --git a/packages/cli/snap-tests/create-org-bundled-dotfiles/steps.json b/packages/cli/snap-tests/create-org-bundled-dotfiles/steps.json
new file mode 100644
index 0000000000..7be80d20e2
--- /dev/null
+++ b/packages/cli/snap-tests/create-org-bundled-dotfiles/steps.json
@@ -0,0 +1,8 @@
+{
+  "commands": [
+    "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",
+    "ls -A my-demo-app # verify _gitignore/_npmrc were renamed and no underscore variants remain",
+    "cat my-demo-app/.gitignore # verify _gitignore content was preserved",
+    "cat my-demo-app/.npmrc # verify _npmrc content was preserved"
+  ]
+}
diff --git a/packages/cli/snap-tests/create-org-bundled-dotfiles/tarballs/create-1.0.0.tgz b/packages/cli/snap-tests/create-org-bundled-dotfiles/tarballs/create-1.0.0.tgz
new file mode 100644
index 0000000000000000000000000000000000000000..33fb4f418da11208ae870b9c50d255e8a6e86db6
GIT binary patch
literal 496
zcmV<M0T2EkiwFSVWd&*g1MQbVZ<{a_$9vAFc*>=h1q>LFI#t@%M%tlMrrxG$LI}Ie
z8`#J;S*NMreKye0bxU&?V(Q}m<wbsf^B$1jbJFC+*Sw^D(?{=%68vOhA%qf)!Neeq
z(ZkpaM==C(h=MQ)lOzHZ;1q`^fTnKbPimFUrD>&AR6gx{I^W+4?BX-gj|ISz=QUlP
z!SdC2(aM#OrC&&zYr1@4vs)?^6WkgC4|%9xB~%rXHG0T=Fgsx*OVG4#DzjJhw*EpF
zg5R*x_-r||I-1VlW48oDs!QMZLuf6!%M9y}YQAYXsVD(!+tr0{On(L<^bM<qCWhVj
z4!hO!cP;o`mBDt`#ZL?S>2C3_U(O5~xPSCGrX2@|_z!R}@}CBV|1^nX$NwDqh5szO
zT)*37@2_58tlw^~F4xyt_S+0V;|%C&R3q1fZmng@3}wf|&~w%v{<D&4R&pWf1T_xv
zkHT=re~MDeKaLW|{~Y3i(5x1uHHYPbFr^)^N2PuI^QM-?L^TfbAEA-|5F7rJIKhtp
zIh40rtQa>Bp{iC5HOKt9mMwKt;7GKGe<cf_aYAoBt^T^kVgB)h^S}T7A0fy89HO_4
mkQxeMMgZ9V0dNkd_A7Au(#-%TCnu->SU&*>PkB=S7ytmv*!jBv

literal 0
HcmV?d00001

