From f9eb1f6f64d5aa48a23ec521bd025a1dc4152aaa Mon Sep 17 00:00:00 2001 From: Oskar Eichler <62393985+OskarEichler@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:07:31 +0300 Subject: [PATCH] fix(cli): complete template copies and preserve literal project paths --- .../cli/src/commands/init/editTemplate.ts | 68 ++++++++----------- packages/cli/src/commands/init/template.ts | 7 +- packages/cli/src/tools/copyFiles.ts | 56 ++++----------- packages/cli/src/tools/walk.ts | 10 ++- 4 files changed, 52 insertions(+), 89 deletions(-) diff --git a/packages/cli/src/commands/init/editTemplate.ts b/packages/cli/src/commands/init/editTemplate.ts index d3bb39f23..30ff67e87 100644 --- a/packages/cli/src/commands/init/editTemplate.ts +++ b/packages/cli/src/commands/init/editTemplate.ts @@ -27,11 +27,15 @@ export function validatePackageName(packageName: string) { /^([a-zA-Z]([a-zA-Z0-9_])*\.)+[a-zA-Z]([a-zA-Z0-9_])*$/u; if (packageNameParts.length < 2) { - throw `The package name ${packageName} is invalid. It should contain at least two segments, e.g. com.app`; + throw new CLIError( + `The package name ${packageName} is invalid. It should contain at least two segments, e.g. com.app`, + ); } if (!packageNameRegex.test(packageName)) { - throw `The ${packageName} package name is not valid. It can contain only alphanumeric characters and dots.`; + throw new CLIError( + `The ${packageName} package name is not valid. It can contain only alphanumeric characters and dots.`, + ); } } @@ -43,9 +47,8 @@ export async function replaceNameInUTF8File( logger.debug(`Replacing in ${filePath}`); const fileContent = await fs.readFile(filePath, 'utf8'); const replacedFileContent = fileContent - .replace(new RegExp(templateName, 'g'), projectName) - .replace( - new RegExp(templateName.toLowerCase(), 'g'), + .replace(new RegExp(templateName, 'g'), () => projectName) + .replace(new RegExp(templateName.toLowerCase(), 'g'), () => projectName.toLowerCase(), ); @@ -57,7 +60,7 @@ export async function replaceNameInUTF8File( async function renameFile(filePath: string, oldName: string, newName: string) { const newFileName = path.join( path.dirname(filePath), - path.basename(filePath).replace(new RegExp(oldName, 'g'), newName), + path.basename(filePath).replace(new RegExp(oldName, 'g'), () => newName), ); logger.debug(`Renaming ${filePath} -> file:${newFileName}`); @@ -70,11 +73,16 @@ function shouldRenameFile(filePath: string, nameToReplace: string) { } function shouldIgnoreFile(filePath: string) { - return filePath.match(/node_modules|yarn.lock|package-lock.json/g); + return path + .relative(process.cwd(), filePath) + .split(path.sep) + .some((part) => + ['node_modules', 'yarn.lock', 'package-lock.json'].includes(part), + ); } function isIosFile(filePath: string) { - return filePath.includes('ios'); + return path.relative(process.cwd(), filePath).split(path.sep)[0] === 'ios'; } const UNDERSCORED_DOTFILES = [ @@ -93,7 +101,9 @@ const UNDERSCORED_DOTFILES = [ ]; async function processDotfiles(filePath: string) { - const dotfile = UNDERSCORED_DOTFILES.find((e) => filePath.includes(`_${e}`)); + const dotfile = UNDERSCORED_DOTFILES.find( + (e) => path.basename(filePath) === `_${e}`, + ); if (dotfile === undefined) { return; @@ -106,36 +116,14 @@ async function createAndroidPackagePaths( filePath: string, packageName: string, ) { - const pathParts = filePath.split('/').slice(-2); - - if (pathParts[0] === 'java' && pathParts[1] === 'com') { - const pathToFolders = filePath.split('/').slice(0, -2).join('/'); - const segmentsList = packageName.split('.'); - - if (segmentsList.length > 1) { - const initialDir = process.cwd(); - process.chdir(filePath.split('/').slice(0, -1).join('/')); - - try { - await fs.rename( - `${filePath}/${segmentsList.join('.')}`, - `${pathToFolders}/${segmentsList[segmentsList.length - 1]}`, - ); - await fs.rmdir(filePath); - - for (const segment of segmentsList) { - fs.mkdirSync(segment); - process.chdir(segment); - } - await fs.rename( - `${pathToFolders}/${segmentsList[segmentsList.length - 1]}`, - process.cwd(), - ); - } catch { - throw 'Failed to create correct paths for Android.'; - } - - process.chdir(initialDir); + const javaPath = path.dirname(filePath); + if (path.basename(javaPath) === 'java' && path.basename(filePath) === 'com') { + const segments = packageName.split('.'); + const destination = path.join(javaPath, ...segments); + await fs.ensureDir(path.dirname(destination)); + await fs.rename(path.join(filePath, packageName), destination); + if (segments[0] !== 'com') { + await fs.rmdir(filePath); } } } @@ -166,7 +154,7 @@ export async function replacePlaceholderWithPackageName({ 'PRODUCT_BUNDLE_IDENTIFIER = "(.*)"', ); - if (filePath.includes('app.json')) { + if (path.basename(filePath) === 'app.json') { await replaceNameInUTF8File(filePath, projectName, placeholderName); } else { const fileExtension = path.extname(filePath); diff --git a/packages/cli/src/commands/init/template.ts b/packages/cli/src/commands/init/template.ts index 7ec4b9841..89f186b04 100644 --- a/packages/cli/src/commands/init/template.ts +++ b/packages/cli/src/commands/init/template.ts @@ -3,7 +3,6 @@ import path from 'path'; import {logger, CLIError} from '@react-native-community/cli-tools'; import * as PackageManager from '../../tools/packageManager'; import copyFiles from '../../tools/copyFiles'; -import replacePathSepForRegex from '../../tools/replacePathSepForRegex'; import fs from 'fs'; import pico from 'picocolors'; import {getYarnVersionIfAvailable} from '../../tools/yarn'; @@ -104,9 +103,11 @@ export async function copyTemplate( ); logger.debug(`Copying template from ${templatePath}`); - let regexStr = path.resolve(templatePath, 'node_modules'); + const nodeModulesPath = path + .resolve(templatePath, 'node_modules') + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); await copyFiles(templatePath, process.cwd(), { - exclude: [new RegExp(replacePathSepForRegex(regexStr))], + exclude: [new RegExp(`^${nodeModulesPath}(?:[/\\\\]|$)`)], }); } diff --git a/packages/cli/src/tools/copyFiles.ts b/packages/cli/src/tools/copyFiles.ts index afcfad842..75a026f6c 100644 --- a/packages/cli/src/tools/copyFiles.ts +++ b/packages/cli/src/tools/copyFiles.ts @@ -7,8 +7,11 @@ import fs from 'fs'; import path from 'path'; +import {promisify} from 'util'; import walk from './walk'; +const copyBinaryFile = promisify(fs.copyFile); + type Options = { exclude?: Array; }; @@ -21,12 +24,13 @@ async function copyFiles( destPath: string, options: Options = {}, ) { + const files = walk( + srcPath, + (filePath) => + options.exclude?.some((p) => filePath.search(p) !== -1) ?? false, + ); return Promise.all( - walk(srcPath).map(async (absoluteSrcFilePath: string) => { - const exclude = options.exclude; - if (exclude && exclude.some((p) => p.test(absoluteSrcFilePath))) { - return; - } + files.map(async (absoluteSrcFilePath: string) => { const relativeFilePath = path.relative(srcPath, absoluteSrcFilePath); await copyFile( absoluteSrcFilePath, @@ -39,7 +43,7 @@ async function copyFiles( /** * Copy a file to given destination. */ -function copyFile(srcPath: string, destPath: string) { +async function copyFile(srcPath: string, destPath: string) { if (fs.lstatSync(srcPath).isDirectory()) { if (!fs.existsSync(destPath)) { fs.mkdirSync(destPath); @@ -48,44 +52,8 @@ function copyFile(srcPath: string, destPath: string) { return; } - return new Promise((resolve, reject) => { - copyBinaryFile(srcPath, destPath, (err) => { - if (err) { - reject(err); - } - resolve(destPath); - }); - }); -} - -/** - * Same as 'cp' on Unix. Don't do any replacements. - */ -function copyBinaryFile( - srcPath: string, - destPath: string, - cb: (err?: Error) => void, -) { - let cbCalled = false; - const {mode} = fs.statSync(srcPath); - const readStream = fs.createReadStream(srcPath); - const writeStream = fs.createWriteStream(destPath, {mode}); - readStream.on('error', (err) => { - done(err); - }); - writeStream.on('error', (err) => { - done(err); - }); - readStream.on('close', () => { - done(); - }); - readStream.pipe(writeStream); - function done(err?: Error) { - if (!cbCalled) { - cb(err); - cbCalled = true; - } - } + await copyBinaryFile(srcPath, destPath); + return destPath; } export default copyFiles; diff --git a/packages/cli/src/tools/walk.ts b/packages/cli/src/tools/walk.ts index 5cc2ea6f1..b1c49478d 100644 --- a/packages/cli/src/tools/walk.ts +++ b/packages/cli/src/tools/walk.ts @@ -9,14 +9,20 @@ import fs from 'fs'; import path from 'path'; -function walk(current: string): string[] { +function walk( + current: string, + exclude?: (filePath: string) => boolean, +): string[] { + if (exclude?.(current)) { + return []; + } if (!fs.lstatSync(current).isDirectory()) { return [current]; } const files = fs .readdirSync(current) - .map((child) => walk(path.join(current, child))); + .map((child) => walk(path.join(current, child), exclude)); const result: string[] = []; return result.concat.apply([current], files); }