Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guide/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
}
}
}
}
21 changes: 21 additions & 0 deletions packages/cli/snap-tests/create-org-bundled-dotfiles/snap.txt
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
]
}
Binary file not shown.
53 changes: 53 additions & 0 deletions packages/cli/src/create/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ensureGitignoreNodeModules,
formatTargetDir,
getProjectDirFromPackageName,
renameFiles,
} from '../utils.js';

describe('getProjectDirFromPackageName', () => {
Expand Down Expand Up @@ -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');
});
});
4 changes: 3 additions & 1 deletion packages/cli/src/create/templates/bundled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -30,6 +30,8 @@ export async function executeBundledTemplate(
throw error;
}

renameFiles(destDir);

try {
setPackageName(destDir, templateInfo.packageName);
} catch {
Expand Down
17 changes: 1 addition & 16 deletions packages/cli/src/create/templates/monorepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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];
Expand Down
16 changes: 16 additions & 0 deletions packages/cli/src/create/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*
Expand Down
Loading