From 762160fc5a04daaa00f29ca3a9d83116bb50a947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A2=85=EA=B2=BD?= 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 = { - _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?= 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 pnpm +→ 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=h1q>LFI#t@%M%tlMrrxG$LI}Ie z8`#J;S*NMreKye0bxU&?V(Q}m&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<^bM2C3_U(O5~xPSCGrX2@|_z!R}@}CBV|1^nX$NwDqh5szO zT)*37@2_58tlw^~F4xyt_S+0V;|%C&R3q1fZmng@3}wf|&~w%v{Bp{iC5HOKt9mMwKt;7GKGeSU&*>PkB=S7ytmv*!jBv literal 0 HcmV?d00001