From 6a319b2b5559813f8647e826e84e15137b0ee6ce Mon Sep 17 00:00:00 2001 From: Muhammad Numan Date: Sun, 13 Feb 2022 15:43:33 +0500 Subject: [PATCH] feat: add support for custom placeholder template --- docs/init.md | 32 ++-- packages/cli/package.json | 2 + .../cli/src/commands/init/editTemplate.ts | 141 +++++++----------- packages/cli/src/commands/init/init.ts | 29 ++-- packages/cli/src/commands/init/template.ts | 26 +--- yarn.lock | 34 +++++ 6 files changed, 119 insertions(+), 145 deletions(-) diff --git a/docs/init.md b/docs/init.md index 741519afc..5a0dfc79d 100644 --- a/docs/init.md +++ b/docs/init.md @@ -76,19 +76,25 @@ Every custom template needs to have configuration file called `template.config.j ```js module.exports = { - // Placeholder name that will be replaced in package.json, index.json, android/, ios/ for a project name. - placeholderName: 'ProjectName', - - // Placeholder title that will be replaced in values.xml and Info.plist with title provided by the user. - // We default this value to 'Hello App Display Name', which is default placeholder in react-native template. - titlePlaceholder: 'Hello App Display Name', - - // Directory with the template which will be copied and processed by React Native CLI. Template directory should have package.json with all dependencies specified, including `react-native`. - templateDir: './template', - - // Path to script, which will be executed after initialization process, but before installing all the dependencies specified in the template. This script runs as a shell script but you can change that (e.g. to Node) by using a shebang (see example custom template). - postInitScript: './script.js', + templateDir: "./template", + + placeholders: { + hermes_flag:true, + + slug:"my_project_name", + + // Placeholder name that will be replaced in package.json, index.json, android/, ios/ for a project name. + name: 'MyProjectName', // if you override this name, than you can use --title arg in cli + + // title that will be replaced in values.xml and Info.plist with title provided by the user. + // We default this value to 'Hello App Display Name', which is default placeholder in react-native template. + title: 'Hello App Display Name' + }, + // Path to script, which will be executed after init + postInitScript: "./script.js" }; ``` -You can find example custom template [here](https://github.com/Esemesek/react-native-new-template). +You can find example custom template [here](https://github.com/nomi9995/react-native-template-placeholder). + +for add more placeholders, you can add a placeholder into the `placeholders` object as mentioned in `template.config.js` and you can add that variable in template file like this `<%= slug %>` by using [Embedded JavaScript templating](https://ejs.co/#docs) diff --git a/packages/cli/package.json b/packages/cli/package.json index 06a4f0f29..4a20ef474 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -34,6 +34,7 @@ "@react-native-community/cli-config": "^7.0.1", "chalk": "^4.1.2", "commander": "^2.19.0", + "ejs": "^3.1.6", "execa": "^1.0.0", "find-up": "^4.1.0", "fs-extra": "^8.1.0", @@ -48,6 +49,7 @@ "react-native": "*" }, "devDependencies": { + "@types/ejs": "^3.1.0", "@types/fs-extra": "^8.1.0", "@types/graceful-fs": "^4.1.3", "@types/hapi__joi": "^17.1.6", diff --git a/packages/cli/src/commands/init/editTemplate.ts b/packages/cli/src/commands/init/editTemplate.ts index cf3a01c15..df5625ea2 100644 --- a/packages/cli/src/commands/init/editTemplate.ts +++ b/packages/cli/src/commands/init/editTemplate.ts @@ -1,113 +1,72 @@ import path from 'path'; import {logger} from '@react-native-community/cli-tools'; -import walk from '../../tools/walk'; - +import ejs from 'ejs'; +const BINARIES = /(gradlew|\.(jar|keystore|png|jpg|gif))$/; // We need `graceful-fs` behavior around async file renames on Win32. // `gracefulify` does not support patching `fs.promises`. Use `fs-extra`, which // exposes its own promise-based interface over `graceful-fs`. import fs from 'fs-extra'; -interface PlaceholderConfig { - projectName: string; - placeholderName: string; - placeholderTitle?: string; - projectTitle?: string; -} - -/** - TODO: This is a default placeholder for title in react-native template. - We should get rid of this once custom templates adapt `placeholderTitle` in their configurations. -*/ -const DEFAULT_TITLE_PLACEHOLDER = 'Hello App Display Name'; +type placeholdersType = {[key: string]: any}; -async function replaceNameInUTF8File( - filePath: string, - projectName: string, - templateName: string, +export function overridePlaceholderTitle( + projectTitle?: string, + placeholders?: placeholdersType, ) { - 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'), - projectName.toLowerCase(), - ); - - if (fileContent !== replacedFileContent) { - await fs.writeFile(filePath, replacedFileContent, 'utf8'); + if (projectTitle && placeholders) { + placeholders.title = projectTitle; } } -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), +export async function copyTemplateAndReplacePlaceholders( + templateName: string, + templateDir: string, + templateSourceDir: string, + placeholders: placeholdersType = {}, +) { + const templatePath = path.resolve( + templateSourceDir, + 'node_modules', + templateName, + templateDir, ); - logger.debug(`Renaming ${filePath} -> file:${newFileName}`); - - await fs.rename(filePath, newFileName); -} + const dest = process.cwd(); -function shouldRenameFile(filePath: string, nameToReplace: string) { - return path.basename(filePath).includes(nameToReplace); -} + logger.debug( + `Copying template from ${templatePath} and replace placeholders`, + ); -function shouldIgnoreFile(filePath: string) { - return filePath.match(/node_modules|yarn.lock|package-lock.json/g); + await CopyDirWithReplacePlaceholders(templatePath, dest, placeholders); } -const UNDERSCORED_DOTFILES = [ - 'buckconfig', - 'eslintrc.js', - 'flowconfig', - 'gitattributes', - 'gitignore', - 'prettierrc.js', - 'watchmanconfig', - 'editorconfig', - 'bundle', - 'ruby-version', -]; - -async function processDotfiles(filePath: string) { - const dotfile = UNDERSCORED_DOTFILES.find((e) => filePath.includes(`_${e}`)); - - if (dotfile === undefined) { - return; - } - - await renameFile(filePath, `_${dotfile}`, `.${dotfile}`); -} +export const CopyDirWithReplacePlaceholders = async ( + source: string, + dest: string, + placeholders: placeholdersType = {}, +) => { + await fs.mkdirp(dest); + + const files = await fs.readdir(source); + for (const f of files) { + const target = path.join( + dest, + ejs.render(f.replace(/^\$/, ''), placeholders, { + openDelimiter: '{', + closeDelimiter: '}', + }), + ); -export async function changePlaceholderInTemplate({ - projectName, - placeholderName, - placeholderTitle = DEFAULT_TITLE_PLACEHOLDER, - projectTitle = projectName, -}: PlaceholderConfig) { - logger.debug(`Changing ${placeholderName} for ${projectName} in template`); + const file = path.join(source, f); + const stats = await fs.stat(file); - for (const filePath of walk(process.cwd()).reverse()) { - if (shouldIgnoreFile(filePath)) { - continue; + if (stats.isDirectory()) { + await CopyDirWithReplacePlaceholders(file, target, placeholders); + } else if (!file.match(BINARIES)) { + const content = await fs.readFile(file, 'utf8'); + await fs.writeFile(target, ejs.render(content, placeholders)); + } else { + await fs.copyFile(file, target); } - if (!(await fs.stat(filePath)).isDirectory()) { - await replaceNameInUTF8File(filePath, projectName, placeholderName); - await replaceNameInUTF8File(filePath, projectTitle, placeholderTitle); - } - if (shouldRenameFile(filePath, placeholderName)) { - await renameFile(filePath, placeholderName, projectName); - } - if (shouldRenameFile(filePath, placeholderName.toLowerCase())) { - await renameFile( - filePath, - placeholderName.toLowerCase(), - projectName.toLowerCase(), - ); - } - - await processDotfiles(filePath); } -} +}; diff --git a/packages/cli/src/commands/init/init.ts b/packages/cli/src/commands/init/init.ts index 416315a62..e3da53aac 100644 --- a/packages/cli/src/commands/init/init.ts +++ b/packages/cli/src/commands/init/init.ts @@ -14,10 +14,12 @@ import { import { installTemplatePackage, getTemplateConfig, - copyTemplate, executePostInitScript, } from './template'; -import {changePlaceholderInTemplate} from './editTemplate'; +import { + copyTemplateAndReplacePlaceholders, + overridePlaceholderTitle, +} from './editTemplate'; import * as PackageManager from '../../tools/packageManager'; import {installPods} from '@react-native-community/cli-doctor'; import banner from './banner'; @@ -34,7 +36,6 @@ type Options = { }; interface TemplateOptions { - projectName: string; templateUri: string; npm?: boolean; directory: string; @@ -76,7 +77,6 @@ function getTemplateName(cwd: string) { } async function createFromTemplate({ - projectName, templateUri, npm, directory, @@ -99,26 +99,19 @@ async function createFromTemplate({ await installTemplatePackage(templateUri, templateSourceDir, npm); loader.succeed(); - loader.start('Copying template'); + loader.start('Copying template & Processing template'); const templateName = getTemplateName(templateSourceDir); const templateConfig = getTemplateConfig(templateName, templateSourceDir); - await copyTemplate( + const placeholders = templateConfig.placeholders || {}; + overridePlaceholderTitle(projectTitle, placeholders); + await copyTemplateAndReplacePlaceholders( templateName, templateConfig.templateDir, templateSourceDir, + placeholders, ); - loader.succeed(); - loader.start('Processing template'); - - await changePlaceholderInTemplate({ - projectName, - projectTitle, - placeholderName: templateConfig.placeholderName, - placeholderTitle: templateConfig.titlePlaceholder, - }); - loader.succeed(); const {postInitScript} = templateConfig; if (postInitScript) { @@ -177,7 +170,6 @@ async function installDependencies({ } async function createProject( - projectName: string, directory: string, version: string, options: Options, @@ -185,7 +177,6 @@ async function createProject( const templateUri = options.template || `react-native@${version}`; return createFromTemplate({ - projectName, templateUri, npm: options.npm, directory, @@ -211,7 +202,7 @@ export default (async function initialize( const directoryName = path.relative(root, options.directory || projectName); try { - await createProject(projectName, directoryName, version, options); + await createProject(directoryName, version, options); const projectFolder = path.join(root, directoryName); printRunInstructions(projectFolder, projectName); diff --git a/packages/cli/src/commands/init/template.ts b/packages/cli/src/commands/init/template.ts index 43db9dcaa..12a75c611 100644 --- a/packages/cli/src/commands/init/template.ts +++ b/packages/cli/src/commands/init/template.ts @@ -2,16 +2,17 @@ import execa from 'execa'; 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 chalk from 'chalk'; export type TemplateConfig = { - placeholderName: string; templateDir: string; postInitScript?: string; titlePlaceholder?: string; + placeholders?: { + placeholderName?: string; + [key: string]: any; + }; }; export async function installTemplatePackage( @@ -57,25 +58,6 @@ export function getTemplateConfig( return require(configFilePath); } -export async function copyTemplate( - templateName: string, - templateDir: string, - templateSourceDir: string, -) { - const templatePath = path.resolve( - templateSourceDir, - 'node_modules', - templateName, - templateDir, - ); - - logger.debug(`Copying template from ${templatePath}`); - let regexStr = path.resolve(templatePath, 'node_modules'); - await copyFiles(templatePath, process.cwd(), { - exclude: [new RegExp(replacePathSepForRegex(regexStr))], - }); -} - export function executePostInitScript( templateName: string, postInitScript: string, diff --git a/yarn.lock b/yarn.lock index d026db245..4ea09625a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2304,6 +2304,11 @@ dependencies: "@types/node" "*" +"@types/ejs@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.0.tgz#ab8109208106b5e764e5a6c92b2ba1c625b73020" + integrity sha512-DCg+Ka+uDQ31lJ/UtEXVlaeV3d6t81gifaVWKJy4MYVVgvJttyX/viREy+If7fz+tK/gVxTGMtyrFPnm4gjrVA== + "@types/errorhandler@^0.0.32": version "0.0.32" resolved "https://registry.yarnpkg.com/@types/errorhandler/-/errorhandler-0.0.32.tgz#387eca73957b481254baddc15d1d40f7fe7153c6" @@ -3005,6 +3010,11 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async@0.9.x: + version "0.9.2" + resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" + integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= + async@^2.4.0: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -4718,6 +4728,13 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= +ejs@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" + integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== + dependencies: + jake "^10.6.1" + electron-to-chromium@^1.3.723: version "1.3.768" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.768.tgz#bbe47394f0073c947168589b7d19388518a7a9a9" @@ -5404,6 +5421,13 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +filelist@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" + integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== + dependencies: + minimatch "^3.0.4" + filesize@^3.6.0: version "3.6.1" resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" @@ -6754,6 +6778,16 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +jake@^10.6.1: + version "10.8.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b" + integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A== + dependencies: + async "0.9.x" + chalk "^2.4.2" + filelist "^1.0.1" + minimatch "^3.0.4" + jest-changed-files@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0"