chrome-extension-cli
Version:
The CLI for your next Chrome Extension.
417 lines (371 loc) • 10.8 kB
JavaScript
;
const path = require('path');
const fs = require('fs-extra');
const chalk = require('chalk');
const spawn = require('cross-spawn');
const commander = require('commander');
const packageFile = require('./package.json');
const { checkAppName, prettifyAppName } = require('./utils/name');
const generateReadme = require('./scripts/readme');
const tryGitInit = require('./scripts/git-init');
let projectName;
const LANGUAGES = ['javascript', 'typescript'];
const OVERRIDE_PAGES = ['newtab', 'bookmarks', 'history'];
const TS_CONFIG = {
compilerOptions: {
rootDir: 'src',
target: 'es6',
module: 'commonjs',
esModuleInterop: true,
strict: true,
},
};
const program = new commander.Command(packageFile.name)
.version(packageFile.version)
.arguments('<project-directory>')
.usage(`${chalk.green('<project-directory>')} [options]`)
.action((name) => {
projectName = name;
})
.option(
'--override-page [page-name]',
'override default page like New Tab, Bookmarks, or History page'
)
.option('--devtools', 'add features to Chrome Developer Tools')
.option('--side-panel', 'add features to Chrome Side Panel')
.option(
'--language [language-name]',
'language like JavaScript and TypeScript'
)
.on('--help', () => {
console.log(` Only ${chalk.green('<project-directory>')} is required.`);
})
.parse(process.argv);
// Exit from the process if no project name is provided
if (typeof projectName === 'undefined') {
console.error('Please specify the project directory:');
console.log(
` ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}`
);
console.log();
console.log('For example:');
console.log(` ${chalk.cyan(program.name())} ${chalk.green('my-extension')}`);
console.log();
console.log(
`Run ${chalk.cyan(`${program.name()} --help`)} to see all options.`
);
process.exit(1);
}
function isOverridePageNameValid(name) {
if (name === true || OVERRIDE_PAGES.includes(name)) {
return true;
}
return false;
}
function isLanguageNameValid(name) {
if (name === true || LANGUAGES.includes(name)) {
return true;
}
return false;
}
function logOverridePageError() {
console.error(
`${chalk.red('Invalid page name passed to option:')} ${chalk.cyan(
'--override-page'
)}`
);
console.log();
console.log(
`You can pass page name as ${chalk.cyan('newtab')}, ${chalk.cyan(
'bookmarks'
)} or ${chalk.cyan('history')}.`
);
console.log();
console.log('For example:');
console.log(
` ${chalk.cyan(program.name())} ${chalk.green(
'my-extension'
)} ${chalk.cyan('--override-page')} ${chalk.green('newtab')}`
);
process.exit(1);
}
function logLanguageError() {
console.error(
`${chalk.red('Invalid language name passed to option:')} ${chalk.cyan(
'--language'
)}`
);
console.log();
console.log(
`You can pass language name as ${chalk.cyan('javascript')} or ${chalk.cyan(
'typescript'
)}.`
);
console.log();
console.log('For example:');
console.log(
` ${chalk.cyan(program.name())} ${chalk.green(
'my-extension'
)} ${chalk.cyan('--language')} ${chalk.green('typescript')}`
);
process.exit(1);
}
function logOptionsConflictError() {
console.error(
`${chalk.red(
'You have passed both "--override-page" and "--devtools" options'
)}`
);
console.log(` ${chalk.cyan('Only pass one of the option')}`);
console.log('');
process.exit(1);
}
function createExtension(
name,
{ overridePage, devtools, sidePanel, language }
) {
const root = path.resolve(name);
let overridePageName;
let languageName = 'javascript';
if (overridePage) {
if (isOverridePageNameValid(overridePage)) {
overridePageName = overridePage === true ? 'newtab' : overridePage;
if (devtools) {
logOptionsConflictError();
}
} else {
logOverridePageError();
}
}
if (language) {
if (isLanguageNameValid(language)) {
languageName = language === true ? 'javascript' : language;
} else {
logLanguageError();
}
}
checkAppName(name);
fs.ensureDirSync(name);
console.log(`Creating a new Chrome extension in ${chalk.green(root)}`);
console.log();
const appDetails = {
version: '0.1.0',
description: 'My Chrome Extension',
};
// Setup the package file
let appPackage = Object.assign(
{},
{
name: name,
},
appDetails,
{
private: true,
}
);
appPackage.scripts = {
watch:
'webpack --mode=development --watch --config config/webpack.config.js',
build: 'webpack --mode=production --config config/webpack.config.js',
pack: 'node pack.js',
repack: 'npm run build && npm run pack',
format:
'prettier --write --ignore-unknown "{config,public,src}/**/*.{html,css,js,ts,json}"',
};
// Create package file in project directory
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(appPackage, null, 2)
);
// Create tsconfig file in project directory
if (languageName === 'typescript') {
fs.writeFileSync(
path.join(root, 'tsconfig.json'),
JSON.stringify(TS_CONFIG, null, 2)
);
}
let command = 'npm';
let args = ['install', '--save-dev'];
// Add devDependencies
args.push(
'webpack@^5.72.0',
'webpack-cli@^4.9.2',
'webpack-merge@^5.8.0',
'copy-webpack-plugin@^10.2.4',
'mini-css-extract-plugin@^2.6.0',
'css-loader@^6.7.1',
'file-loader@^6.2.0',
'prettier@^2.6.2',
'adm-zip@^0.5.10'
);
if (languageName === 'typescript') {
args.push('typescript@4.6.3', 'ts-loader@9.2.8', '@types/chrome@0.0.181');
}
console.log('Installing packages. This might take a couple of minutes.');
console.log(
`Installing ${chalk.cyan('webpack')}, ${chalk.cyan(
'webpack-cli'
)} and few more...`
);
console.log();
// Install package dependencies
const proc = spawn.sync(command, args, { cwd: root, stdio: 'inherit' });
if (proc.status !== 0) {
console.error(`\`${command} ${args.join(' ')}\` failed`);
return;
}
// Copy template files to project directory
let templateName;
if (overridePageName) {
templateName = 'override-page';
} else if (devtools) {
templateName = 'devtools';
} else if (sidePanel) {
templateName = 'side-panel';
} else {
templateName = 'popup';
}
fs.copySync(
path.resolve(__dirname, 'templates', languageName, templateName),
root
);
// Copy common files between languages
fs.copySync(
path.resolve(__dirname, 'templates', 'shared', templateName),
root
);
fs.copySync(
path.resolve(__dirname, 'config', languageName),
path.join(root, 'config')
);
// Copy common webpack configuration file
fs.copySync(
path.resolve(__dirname, 'config', 'shared'),
path.join(root, 'config')
);
// Rename gitignore after the fact to prevent npm from renaming it to .npmignore
// See: https://github.com/npm/npm/issues/1862
// Source: https://github.com/facebook/create-react-app/blob/47e9e2c7a07bfe60b52011cf71de5ca33bdeb6e3/packages/react-scripts/scripts/init.js#L138
// Also followed same convention for other dotfiles.
const dotFilesSource = path.resolve(
__dirname,
'templates',
'shared',
'dotfiles'
);
fs.readdirSync(dotFilesSource).map(function (fileName) {
return fs.copyFileSync(
path.join(dotFilesSource, fileName),
path.join(root, '.' + fileName)
);
});
// Copy script to automatically generate zip file
fs.copyFileSync(
path.resolve(__dirname, 'templates', 'shared', 'pack.js'),
path.join(root, 'pack.js')
);
// Setup the manifest file
const manifestDetails = Object.assign(
{},
{
name: prettifyAppName(name),
},
appDetails
);
let appManifest = Object.assign(
{},
{
manifest_version: 3,
},
manifestDetails,
{
icons: {
16: 'icons/icon_16.png',
32: 'icons/icon_32.png',
48: 'icons/icon_48.png',
128: 'icons/icon_128.png',
},
background: {
service_worker: 'background.js',
},
}
);
if (overridePageName) {
appManifest = Object.assign({}, appManifest, {
chrome_url_overrides: {
[overridePageName]: 'index.html',
},
});
} else if (devtools) {
appManifest = Object.assign({}, appManifest, {
devtools_page: 'devtools.html',
});
} else if (sidePanel) {
appManifest = Object.assign({}, appManifest, {
side_panel: {
default_path: 'sidepanel.html',
},
permissions: ['sidePanel', 'tabs'],
});
} else {
appManifest = Object.assign({}, appManifest, {
action: {
default_title: manifestDetails.name,
default_popup: 'popup.html',
},
permissions: ['storage'],
content_scripts: [
{
matches: ['<all_urls>'],
run_at: 'document_idle',
js: ['contentScript.js'],
},
],
});
}
// Create manifest file in project directory
fs.writeFileSync(
path.join(root, 'public', 'manifest.json'),
JSON.stringify(appManifest, null, 2)
);
// Generate a README file
if (generateReadme(manifestDetails, root)) {
console.log('Generated a README file.');
console.log();
}
// Initialize a git repository
if (tryGitInit(root, name)) {
console.log('Initialized a git repository.');
console.log();
}
console.log(`Success! Created ${name} at ${root}`);
console.log('Inside that directory, you can run below commands:');
console.log();
console.log(chalk.cyan(` ${command} run watch`));
console.log(' Listens for files changes and rebuilds automatically.');
console.log();
console.log(chalk.cyan(` ${command} run build`));
console.log(' Bundles the app into static files for Chrome store.');
console.log();
console.log(chalk.cyan(` ${command} run format`));
console.log(' Formats all the files.');
console.log();
console.log('We suggest that you begin by typing:');
console.log();
console.log(` 1. ${chalk.cyan('cd')} ${name}`);
console.log(` 2. Run ${chalk.cyan(`${command} run watch`)}`);
console.log(` 3. Open ${chalk.cyan('chrome://extensions')}`);
console.log(` 4. Check the ${chalk.cyan('Developer mode')} checkbox`);
console.log(
` 5. Click on the ${chalk.cyan('Load unpacked extension')} button`
);
console.log(` 6. Select the folder ${chalk.cyan(name + '/build')}`);
console.log();
}
createExtension(projectName, {
overridePage: program.overridePage,
devtools: program.devtools,
sidePanel: program.sidePanel,
language: program.language,
});