@rapido/scripts
Version:
A library of scripts useed to develop Rapido apps.
463 lines (414 loc) • 13.4 kB
JavaScript
// @remove-file-on-eject
/**
* Copyright (c) 2019-present Verum Technologies
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
;
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
const os = require('os');
const path = require('path');
const fs = require('fs-extra');
const execSync = require('child_process').execSync;
const chalk = require('@rapido/dev-utils/chalk');
const spawn = require('@rapido/dev-utils/crossSpawn');
const sortPackageJson = require('@rapido/dev-utils/sortPackageJson');
const { defaultBrowsers } = require('@rapido/dev-utils/browsersHelper');
const verifyTypeScriptSetup = require('../config/verifyTypeScriptSetup');
function isInGitRepository() {
try {
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
function isInMercurialRepository() {
try {
execSync('hg --cwd . root', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
function tryGitInit(appPath) {
let didInit = false;
try {
execSync('git --version', { stdio: 'ignore' });
if (isInGitRepository() || isInMercurialRepository()) {
return false;
}
execSync('git init', { stdio: 'ignore' });
didInit = true;
execSync('git add -A', { stdio: 'ignore' });
execSync('git commit -m "Initial commit from Rapido"', {
stdio: 'ignore',
});
return true;
} catch (e) {
if (didInit) {
// If we successfully initialized but couldn't commit,
// maybe the commit author config is not set.
// In the future, we might supply our own committer
// like Ember CLI does, but for now, let's just
// remove the Git files to avoid a half-done state.
try {
// unlinkSync() doesn't work on directories.
fs.removeSync(path.join(appPath, '.git'));
} catch (removeErr) {
// Ignore.
}
}
return false;
}
}
function walk(dir, done) {
var folders = [];
var files = [];
fs.readdir(dir, function(err, list) {
if (err) {
return done(err);
}
var pending = list.length;
if (!pending) {
return done(null, folders, files);
}
list.forEach(function(file) {
file = path.resolve(dir, file);
fs.stat(file, function(err, stat) {
if (stat && stat.isDirectory()) {
folders.push(file);
walk(file, function(err, resFolders, resFiles) {
folders = folders.concat(resFolders);
files = files.concat(resFiles);
if (!--pending) {
done(null, folders, files);
}
});
} else {
files.push(file);
if (!--pending) {
done(null, folders, files);
}
}
});
});
});
}
function filterContent(content, key, useKey) {
let filteredContent = content;
let regex = new RegExp(`// @remove-file-if-no-${key}\\n?`);
filteredContent =
!useKey && filteredContent.match(regex)
? ''
: filteredContent.replace(regex, '');
regex = new RegExp(`// @remove-file-if-${key}\\n?`);
filteredContent =
useKey && filteredContent.match(regex)
? ''
: filteredContent.replace(regex, '');
regex = RegExp(
`\\/\\/ @remove-if-no-${key}-begin\\n?([\\s\\S]*?)\\/\\/ @remove-if-no-${key}-end\\n?`,
'gm'
);
filteredContent = filteredContent.replace(regex, useKey ? '$1' : '');
regex = RegExp(
`\\/\\/ @remove-if-${key}-begin\\n?([\\s\\S]*?)\\/\\/ @remove-if-${key}-end\\n?`,
'gm'
);
filteredContent = filteredContent.replace(regex, useKey ? '' : '$1');
return filteredContent;
}
module.exports = function(
appPath,
appName,
verbose,
originalDirectory,
templateName,
usePrettier,
useComponents,
useEnv,
useSession,
useUtils
) {
const appPackage = require(path.join(appPath, 'package.json'));
const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));
if (!templateName) {
console.log('');
console.error('A template was not provided.');
return;
}
const templatePath = path.join(
require.resolve(templateName, { paths: [appPath] }),
'..'
);
let command;
let remove;
let args;
if (useYarn) {
command = 'yarnpkg';
remove = 'remove';
args = ['add'];
} else {
command = 'npm';
remove = 'uninstall';
args = ['install', '--save', verbose && '--verbose'].filter(e => e);
}
// Install prettier dependencies, if enabled
if (usePrettier) {
args = args.concat(['prettier', 'husky', 'lint-staged']);
}
// Install additional template dependencies, if present
const templateJsonPath = path.join(templatePath, 'template.json');
if (fs.existsSync(templateJsonPath)) {
const templateDependencies = require(templateJsonPath).dependencies;
args = args.concat(
Object.keys(templateDependencies).map(key => {
const version = templateDependencies[key];
return version ? `${key}@${version}` : key;
})
);
fs.unlinkSync(templateJsonPath);
}
const useTypeScript = args.find(arg => arg.includes('typescript'));
// Copy over some of the devDependencies
appPackage.dependencies = appPackage.dependencies || {};
// Set main script
appPackage.main = 'node_modules/expo/AppEntry.js';
// Setup the script rules
appPackage.scripts = {
'build:android': 'rapido build --android',
'build:ios': 'rapido build --ios',
'build:web': 'rapido build --web',
eject: 'rapido eject',
lint: 'rapido lint',
publish: 'rapido publish',
'start:android': 'rapido start --android',
'start:ios': 'rapido start --ios',
'start:web': 'rapido start --web',
test: 'rapido test',
upgrade: 'rapido upgrade',
};
if (useTypeScript) {
appPackage.scripts.tsc = 'tsc --noEmit';
}
if (usePrettier) {
appPackage.scripts.format = `prettier --trailing-comma es5 --single-quote --write '**/*.{js,jsx,ts,tsx,json,css,scss,md}'`;
appPackage.husky = {
hooks: {
'pre-commit': 'lint-staged',
},
};
appPackage['lint-staged'] = {
'*.{js,jsx,ts,tsx,json,css,scss,md}': [
'prettier --trailing-comma es5 --single-quote --write',
'git add',
],
};
appPackage.prettier = {
singleQuote: true,
trailingComma: 'es5',
};
}
// Setup the eslint config
appPackage.eslintConfig = {
extends: 'react-app',
};
// Setup the browsers list
appPackage.browserslist = defaultBrowsers;
fs.writeFileSync(
path.join(appPath, 'package.json'),
JSON.stringify(sortPackageJson(appPackage), null, 2) + os.EOL
);
const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
if (readmeExists) {
fs.renameSync(
path.join(appPath, 'README.md'),
path.join(appPath, 'README.old.md')
);
}
// Copy the files with assets for the user
const templateDir = path.join(templatePath, 'template');
function verifyAbsent(file) {
if (fs.existsSync(path.join(appPath, file.replace(templateDir, '')))) {
console.error(
`\`${file.replace(
templateDir,
''
)}\` already exists in your app folder. We cannot ` +
'continue as you would lose all the changes in that file or directory. ' +
'Please move or delete it (maybe make a copy for backup) and run this ' +
'command again.'
);
process.exit(1);
}
}
if (fs.existsSync(templateDir)) {
walk(templateDir, function(err, folders, files) {
if (err) {
console.error(
`Could not read template directory: ${chalk.green(templateDir)}`
);
return;
}
// Ensure that the app folder is clean and we won't override any files
folders.forEach(verifyAbsent);
files.forEach(verifyAbsent);
folders.forEach(folder => {
fs.mkdirSync(folder.replace(templateDir, appPath));
});
files.forEach(file => {
let mediaRegex = new RegExp(/[\w-]+.(jpg|jpeg|png|ico|gif|mov|svg)/g);
if (file.match(mediaRegex)) {
fs.copySync(file, file.replace(templateDir, appPath));
return;
}
const original = fs.readFileSync(file, 'utf8');
let content = filterContent(original, 'components', useComponents);
content = filterContent(content, 'env', useEnv);
content = filterContent(content, 'session', useSession);
content = filterContent(content, 'utils', useUtils);
if (!content) {
return;
}
fs.writeFileSync(
file.replace(templateDir, appPath),
content.trim() + '\n'
);
});
// modifies README.md commands based on user used package manager.
if (useYarn) {
try {
const readme = fs.readFileSync(
path.join(appPath, 'README.md'),
'utf8'
);
fs.writeFileSync(
path.join(appPath, 'README.md'),
readme
.replace(/npm start:<platform>/g, 'yarn start:<platform>')
.replace(/npm lint/g, 'yarn lint')
.replace(/npm test/g, 'yarn test')
.replace(/npm build:<platform>/g, 'yarn build:<platform>')
.replace(/npm run publish/g, 'yarn run publish')
.replace(/npm run upgrade/g, 'yarn run upgrade')
.replace(/npm run eject/g, 'yarn eject'),
'utf8'
);
} catch (err) {
// Silencing the error. As it fall backs to using default npm commands.
}
}
// Rename gitignore after the fact to prevent npm from renaming it to .npmignore
// See: https://github.com/npm/npm/issues/1862
try {
fs.moveSync(
path.join(appPath, 'gitignore'),
path.join(appPath, '.gitignore'),
[]
);
} catch (err) {
// Append if there's already a `.gitignore` file there
if (err.code === 'EEXIST') {
const data = fs.readFileSync(path.join(appPath, 'gitignore'));
fs.appendFileSync(path.join(appPath, '.gitignore'), data);
fs.unlinkSync(path.join(appPath, 'gitignore'));
} else {
throw err;
}
}
if (args.length > 1) {
console.log();
console.log(`Installing template dependencies using ${command}...`);
const proc = spawn.sync(command, args, { stdio: 'inherit' });
if (proc.status !== 0) {
console.error(`\`${command} ${args.join(' ')}\` failed`);
return;
}
}
if (useTypeScript) {
console.log();
verifyTypeScriptSetup();
}
// Remove template
console.log(`Removing template package using ${command}...`);
console.log();
const proc = spawn.sync(command, [remove, templateName], {
stdio: 'inherit',
});
if (proc.status !== 0) {
console.error(`\`${command} ${args.join(' ')}\` failed`);
return;
}
if (usePrettier) {
const prettierCmd = path.resolve(appPath, 'node_modules/.bin/prettier');
const prettierArgs = [
'--trailing-comma',
'es5',
'--single-quote',
'--write',
`${appPath}**/*.{js,jsx,ts,tsx,json,css,scss,md}`,
];
const prettierProc = spawn.sync(prettierCmd, prettierArgs, {
stdio: 'inherit',
});
if (prettierProc.status !== 0) {
console.error(`\`${command} ${args.join(' ')}\` failed`);
return;
}
}
if (tryGitInit(appPath)) {
console.log();
console.log('Initialized a git repository.');
}
// Display the most elegant way to cd.
// This needs to handle an undefined originalDirectory for
// backward compatibility with old global-cli's.
let cdpath;
if (
originalDirectory &&
path.join(originalDirectory, appName) === appPath
) {
cdpath = appName;
} else {
cdpath = appPath;
}
// Change displayed command to yarn instead of yarnpkg
const displayedCommand = useYarn ? 'yarn' : 'npm';
console.log();
console.log(`Success! Created ${appName} at ${appPath}`);
console.log('Inside that directory, you can run several commands.');
console.log();
console.log('We suggest that you begin by typing:');
console.log();
console.log(chalk.cyan(' cd'), cdpath);
console.log(` ${chalk.cyan(`${displayedCommand} start:web`)}`);
if (readmeExists) {
console.log();
console.log(
chalk.yellow(
'You had a `README.md` file, we renamed it to `README.old.md`'
)
);
}
console.log();
console.log(
'Please refer to the official Rapido documentation for the full user guide: ',
chalk.cyan('https://rapidojs.org/')
);
console.log();
console.log('Happy hacking!');
});
} else {
console.error(
`Could not locate supplied template: ${chalk.green(templateDir)}`
);
return;
}
};