@ngx-rocket/core
Version:
Core generator for creating ngX-Rocket add-ons
324 lines (284 loc) • 10.8 kB
JavaScript
;
const path = require('path');
const process = require('process');
const fs = require('fs');
const _ = require('lodash');
const chalk = require('chalk');
const Generator = require('yeoman-generator');
// Restore install action for backward compatibility
_.extend(Generator.prototype, require('yeoman-generator/lib/actions/install'));
const FileUtilities = require('./file-utilities.js');
const SharedStorageKey = Symbol.for('@ngx-rocket/core');
if (!global[SharedStorageKey]) {
// Initialize shared storage only once
global[SharedStorageKey] = {
props: {},
fullstack: false,
instances: 0
};
}
class CoreGenerator extends Generator {
/**
* Creates a new Yeoman generator extending the core ngx-rocket generator.
* @param {object} options Configures your generator instance:
* - `baseDir`: base directory for your generator templates
* - `generator`: your generator base class (optional)
* - `options`: generator options, see related section at http://yeoman.io/authoring/user-interactions.html (optional).
* - `prompts`: generator prompts, using [Inquirer.js](https://github.com/SBoudrias/Inquirer.js#question) format (optional).
* - `templatesDir`: generator templates directory (optional, default: 'templates')
* - `prefixRules`: generator template prefix rules (optional, default: defaultPrefixRules)
* - `toolsFilter`: file filter patterns to use when toolchain only option is enabled. If not provided, the generator
* will try to load the `.toolsignore` file inside `baseDir`.
* - `type`: generator type, can be `client`, `server` or `fullstack` (optional, default: 'client'). In `fullstack`
* mode, client and server templates must be separated into `client`, `server` and `root` subfolders.
* @return {Generator} A new Yeoman generator derived from the specified options.
*/
static make(options) {
if (!options.baseDir) {
throw new Error("You must provide a 'baseDir' property");
}
return class extends CoreGenerator {
constructor(args, generatorOptions, features) {
super(args, generatorOptions, features, options.options, options.prompts);
global[SharedStorageKey].instances++;
if (options.type === 'server' || options.type === 'fullstack') {
global[SharedStorageKey].fullstack = true;
} else {
options.type = 'client';
}
this._type = options.type;
this._templatesPath = path.join(options.baseDir, options.templatesDir || 'templates');
this._prefixRules = options.prefixRules || CoreGenerator.defaultPrefixRules;
if (generatorOptions.tools) {
const ignoreFile = path.join(options.baseDir, '.toolsignore');
if (options.toolsFilter) {
// Use filter patterns provided via options
this._toolsFilter = options.toolsFilter;
} else if (fs.existsSync(ignoreFile)) {
this._toolsFilter = fs.readFileSync(ignoreFile).toString();
}
}
// Core methods
const proto = Object.getPrototypeOf(this);
['prompting', 'writing'].forEach((method) => {
proto[method] = super[method];
});
// Additional methods
const generatorBase = options.generator;
if (generatorBase) {
Object.getOwnPropertyNames(generatorBase.prototype)
.filter((method) => method.charAt(0) !== '_' && method !== 'constructor')
.forEach((method) => {
proto[method] = generatorBase.prototype[method];
});
}
}
};
}
/**
* Gets the default prefix rules.
* @return {Object} The default prefix rules.
*/
static get defaultPrefixRules() {
return {
web: (props) => props.target.includes('web'),
cordova: (props) => props.target.includes('cordova'),
electron: (props) => props.target.includes('electron'),
pwa: (props) => Boolean(props.pwa),
bootstrap: (props) => props.ui === 'bootstrap',
ionic: (props) => props.ui === 'ionic',
material: (props) => props.ui === 'material',
raw: (props) => props.ui === 'raw',
universal: (props) => Boolean(props.universal),
auth: (props) => Boolean(props.auth),
ios: (props) => props.mobile && props.mobile.includes('ios'),
android: (props) => props.mobile && props.mobile.includes('android'),
windows: (props) => props.mobile && props.mobile.includes('windows')
};
}
/**
* Gets a copy of properties shared between generators.
* @return {Object} A copy of shared properties.
*/
static get sharedProps() {
return _.assign({}, global[SharedStorageKey].props);
}
/**
* Sets additional shared properties between generators.
* To avoid collisions issues, only properties that are currently undefined will be added.
* @param {Object} props The additional shared properties to set.
*/
static shareProps(props) {
_.defaults(global[SharedStorageKey].props, props);
}
constructor(args, generatorOptions, features, options, prompts) {
super(args, generatorOptions, features);
options = options || [];
prompts = prompts || [];
// Default options for all generators
options = [
...options,
{
name: 'update',
type: 'Boolean',
required: false,
description: 'Update existing project',
defaults: true
},
{
name: 'automate',
type: 'String',
required: false,
description: 'Automate prompt answers using the specified JSON file',
defaults: ''
},
{
name: 'packageManager',
type: (value) => {
if (value !== 'yarn' && value !== 'npm') {
console.error('Invalid package manager: can be either "yarn" or "npm"');
// eslint-disable-next-line unicorn/no-process-exit
process.exit(-1);
}
return value;
},
description: 'Choose Yarn or NPM as package manager',
defaults: process.env.NGX_PACKAGE_MANAGER || 'npm'
},
{
name: 'tools',
type: 'Boolean',
required: false,
description: 'Generate only the toolchain',
defaults: false
}
];
// Add given options
options.forEach((option) => {
this.option(option.name, {
type: typeof option.type === 'function' ? option.type : global[option.type],
required: option.required,
description: option.description || option.desc,
defaults: option.defaults
});
});
this._corePrompts = prompts;
this.props = {};
}
/**
* Gets a copy of properties shared between generators.
* @return {Object} A copy of shared properties.
*/
get sharedProps() {
return CoreGenerator.sharedProps;
}
/**
* Sets additional shared properties between generators.
* To avoid collisions issues, only properties that are currently undefined will be added.
* @param {Object} props The additional shared properties to set.
*/
shareProps(props) {
CoreGenerator.shareProps(props);
}
/**
* Checks if the generator is running standalone.
* @return {boolean} `true` if the generator is running standalone or `false` if it is running as an add-on.
*/
get isStandalone() {
return global[SharedStorageKey].instances === 1;
}
/**
* Checks if this or a composed generator has declared to be in `server` or `fullstack` mode.
* @return {boolean} `true` if this or a composed generator has declared to be in `server` or `fullstack` mode or `false` if it
* is running in client only mode.
*/
get isFullstack() {
return global[SharedStorageKey].fullstack;
}
/**
* Gets the the package manager to use.
* @return {'npm'|'yarn'} Returns the package manager to use (either `npm` or `yarn`)
*/
get packageManager() {
return this.options.packageManager || 'npm';
}
async prompting() {
// Load saved props if updating
if (this.options.update) {
this.props = this.config.get('props') || {};
}
const processProps = (props) => {
props.appName = this.sharedProps.appName || this.props.appName || props.appName || this.options.appName;
if (!props.appName) {
throw new Error('appName property must be defined');
}
props.projectName = _.kebabCase(props.appName);
props.packageManager = this.packageManager;
_.assign(this.props, props);
};
if (this.options.automate) {
// Do no prompt, use json file instead
try {
const props = require(path.resolve(this.options.automate));
processProps(props);
} catch {
this.log(chalk.red(`Error: Cannot load "${this.options.automate}"`));
// eslint-disable-next-line unicorn/no-process-exit
process.exit(-1);
}
} else {
const namePrompt = _.find(this._corePrompts, {name: 'appName'});
if (namePrompt) {
namePrompt.default = this.appname;
namePrompt.when = () => !this.options.appName;
}
// Remove prompts for already defined properties
_.remove(this._corePrompts, (p) => this.props[p.name] !== undefined);
const props = await this.prompt(this._corePrompts);
processProps(props);
}
}
async writing() {
if (this.version) {
this.config.set('version', this.version);
}
if (this.props) {
this.config.set('props', this.props);
}
this.config.save();
if (process.env.NGX_CLIENT_PATH === null || process.env.NGX_CLIENT_PATH === undefined) {
process.env.NGX_CLIENT_PATH = 'client';
}
if (process.env.NGX_SERVER_PATH === null || process.env.NGX_SERVER_PATH === undefined) {
process.env.NGX_SERVER_PATH = 'server';
}
const allFiles = await FileUtilities.getFiles(this._templatesPath, this._type);
let files = FileUtilities.prepareFiles(
allFiles.client,
this._templatesPath,
this.isFullstack ? process.env.NGX_CLIENT_PATH : null,
this._type === 'fullstack' ? FileUtilities.ClientTemplatesPath : null
);
files = files.concat(
FileUtilities.prepareFiles(
allFiles.server,
this._templatesPath,
this.isFullstack ? process.env.NGX_SERVER_PATH : null,
this._type === 'fullstack' ? FileUtilities.ServerTemplatesPath : null
)
);
files = files.concat(
FileUtilities.prepareFiles(
allFiles.root,
this._templatesPath,
null,
this._type === 'fullstack' ? FileUtilities.RootTemplatesPath : null
)
);
if (this._toolsFilter) {
files = FileUtilities.filterFiles(files, this._toolsFilter);
}
files.forEach((file) => FileUtilities.writeFile(this, file, this._prefixRules));
}
}
module.exports = CoreGenerator;