@pnp/generator-spfx
Version:
This Yeoman generator helps organisations to improve their development workflow with the SharePoint Framework. It extends the functionalities of the @microsoft/generator-sharepoint based on best pattern and practices. This generator extends the capabiliti
730 lines (519 loc) • 20.4 kB
JavaScript
// include nodejs fs
const fs = require('fs');
// include chalk
const chalk = require('chalk');
// load ejx rendering engine
const ejs = require('ejs');
// check if command-exists
const commandExists = require('command-exists').sync;
// nodejs path
const path = require('path');
// loadash
const _ = require('lodash');
// parameter case
const {
paramCase
} = require('param-case');
// styles
const fgYellow = chalk.yellow;
// telemetry
const telemetry = require('./telemetry');
// TODO: Needs to get updated
// Helper function to sore properties in package.json primarly
const sortProps = (dependencies) => {
var sortedObject = {};
let sortedKeys = Object.keys(dependencies).sort();
for (var i = 0; i < sortedKeys.length; i++) {
sortedObject[sortedKeys[i]] = dependencies[sortedKeys[i]]
}
return sortedObject;
}
class Util {
/**
* removs all comments from a JSON file before updating
*/
_removeJsonComments(jsonContent) {
if (jsonContent !== undefined &&
jsonContent !== null) {
var commentEval = new RegExp(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm);
return jsonContent.replace(commentEval, '');
} else {
return null;
}
}
/**
* Sort properties of dependencies
*/
_sortDependencies(dependencies) {
var sortedObject = {};
let sortedKeys = Object.keys(dependencies).sort();
for (var i = 0; i < sortedKeys.length; i++) {
sortedObject[sortedKeys[i]] = dependencies[sortedKeys[i]]
}
return sortedObject;
}
/**
* Project update check used from Office 365 cli
*/
projectUpgrade(projectFolder, format, generatorVersion) {
const validFormats = ['json', 'text', 'md', 'tour'];
if (validFormats.indexOf(format) === -1) {
throw 'Format parameter error: possible values are text, md, json, tour'
}
// init instance of upgarde command
const o365CliUpgrade = require("@pnp/office365-cli/dist/o365/spfx/commands/project/project-upgrade.js");
// change current folder in process to project folder
process.chdir(projectFolder);
// store log result or error
let upgradeCheckResult = {
log: null,
error: null
}
// create a new command instance to check the project
const cmdInstance = {
log: (result) => {
if (Array.isArray(result)) {
upgradeCheckResult.error = result
} else {
upgradeCheckResult.log = result;
}
},
error: (err) => {
if (err !== undefined) {
upgradeCheckResult.error = err;
}
}
}
// define upgrade settings of current solution
const upgradeSettings = {
options: {
toVersion: generatorVersion,
projectRootPath: projectFolder,
output: format
}
}
// execute the update check
o365CliUpgrade.commandAction(
cmdInstance,
upgradeSettings,
(err) => {
cmdInstance.error(err);
});
return upgradeCheckResult;
}
/**
* performs a project update check
* @param {*} yeoman
* @param {*} projectFolder
* @param {*} generatorVersion
*/
projectStatusCheck(yeoman, projectFolder, generatorVersion) {
if (yeoman.options.skipVersionCheck) {
return true;
}
const checkResult = this.projectUpgrade(projectFolder, 'json', generatorVersion);
if (checkResult.log !== null) {
yeoman.log(chalk.green(checkResult.log))
return true;
} else {
yeoman.log(
fgYellow('!!! Project needs to be upgraded !!!')
);
const mdLog = this.projectUpgrade(projectFolder, 'md', generatorVersion);
fs.writeFileSync('upgrade-log.md', mdLog.log, 'utf-8');
// const tour = this.projectUpgrade(projectFolder, 'tour', generatorVersion);
// console.log('Tour test ::: ', tour);
// if (!fs.existsSync('./.tour')) {
// fs.mkdirSync('./.tour');
// }
// fs.writeFileSync('./.tour/upgrade.tour', tour.loog, 'utf-8');
yeoman.log('> Please review the file ', fgYellow("upgrade-log.md"), ' in the project root.\n');
return false;
}
}
/**
* Merge addons in package.json
*/
mergeAddons(addonConfig, requestedLibraries, config) {
let dependencies = config.dependencies;
let devDependencies = config.devDependencies;
// let rmDependencies = config.rmDependencies;
for (let key in addonConfig) {
if (requestedLibraries.indexOf(key) !== -1) {
// inject dependencies
if (addonConfig[key].dependencies) {
for (let depend in addonConfig[key].dependencies) {
dependencies[depend] = addonConfig[key].dependencies[depend];
}
}
// inject dev dependencies
if (addonConfig[key].devDependencies) {
for (let depend in addonConfig[key].devDependencies) {
devDependencies[depend] = addonConfig[key].devDependencies[depend];
}
}
// adding dev dependencies
}
}
// Remove certain package dependencies
requestedLibraries.forEach(item => {
if (typeof addonConfig[item] !== undefined &&
addonConfig[item].rmDependencies !== undefined) {
dependencies = this.removeDependencies(dependencies, addonConfig[item].rmDependencies);
devDependencies = this.removeDependencies(devDependencies, addonConfig[item].rmDependencies);
}
});
// sort package properties
let sortedDependencies = sortProps(dependencies);
let sortedDevDependencies = sortProps(devDependencies);
// assing sorted dependencies
config.dependencies = sortedDependencies;
config.devDependencies = sortedDevDependencies;
// return new configuration
return config;
}
// Remove devDependencies and dependencies for package.json
removeDependencies(dependencies, dependencies2remove) {
Object.keys(dependencies).forEach(key => {
if (dependencies2remove.indexOf(key) !== -1) {
delete dependencies[key];
}
});
return dependencies;
}
/**
* compose new gulp file and adds additional stuff
*/
composeGulpFile(coreTemplate, customTemplate, outPutfile, options) {
if (!fs.existsSync(coreTemplate)) {
const error = 'Error: File names ' + coreTemplate + ' cannot be found';
throw error;
}
if (!fs.existsSync(customTemplate)) {
const error = 'Error: File names ' + customTemplate + ' cannot be found';
throw error;
}
let coreTemplatContent = fs.readFileSync(coreTemplate, 'utf-8');
let customTemplateContent = fs.readFileSync(customTemplate, 'utf-8');
// Pass the path options fro EJS Template
options.path = path.join(__dirname,
'../app/templates/gulptasks/');
if (options) {
// Custom template with options
customTemplateContent = ejs.render(customTemplateContent, options);
}
let gulpGileContent = ejs.render(coreTemplatContent, {
customTasks: customTemplateContent,
SpfxOptions: options.SpfxOptions !== undefined ? options.SpfxOptions : {},
options: options
});
try {
fs.writeFileSync(outPutfile, gulpGileContent, 'utf-8');
} catch (error) {
throw error;
}
}
/**
* Install handler in generator
*/
runInstall(yeoman) {
let packageManager = yeoman.options['package-manager'];
if (yeoman.options.skipInstall !== undefined && yeoman.options.skipInstall == true) {
return;
}
if (packageManager === undefined ||
packageManager.toLowerCase() === 'npm' ||
packageManager.toLowerCase() === 'yarn') {
let hasYarn = commandExists('yarn');
// override yarn if npm is preferred
if (packageManager === 'npm') {
hasYarn = false;
// Track install count and version
telemetry.trackEvent("PackageManager", {
"manager": "npm"
})
} else {
// Track install count and version
telemetry.trackEvent("PackageManager", {
"manager": "yarn"
})
}
yeoman.installDependencies({
npm: !hasYarn,
bower: false,
yarn: hasYarn,
skipMessage: yeoman.options['skip-install-message'],
skipInstall: yeoman.options['skip-install']
});
} else {
if (packageManager === 'pnpm') {
// Track install count and version
telemetry.trackEvent("PackageManager", {
"manager": "pnpm"
})
const hasPnpm = commandExists('pnpm');
if (hasPnpm) {
yeoman.spawnCommand('pnpm', ['install']);
} else {
throw 'Cannot find pnpm';
}
} else {
throw 'Error: Package Manager not defined ' + packageManager;
}
}
}
/**
* Check if .yo-rc.json exists in current folder
*/
configExists() {
const cwd = process.cwd(),
yorc = path.join(cwd, '.yo-rc.json');
if (fs.existsSync(yorc)) {
return true;
} else {
return false;
}
}
/**
* React version detections
*/
detectReactVersion(yeoman) {
if (fs.existsSync(yeoman.destinationPath('package.json'))) {
let config = fs.readFileSync(
yeoman.destinationPath('package.json'),
'utf-8'
);
const regReact15 = new RegExp(/react("|'|\ ):(\ |"|')*15./gi),
regReact16 = new RegExp(/react("|'|\ ):(\ |"|')*16./gi);
let targetReact = undefined;
if (config.match(regReact15) !== null) {
targetReact = "react15";
}
if (config.match(regReact16) !== null) {
targetReact = "react16";
}
return targetReact;
} else {
throw new Exception("Config couldn't be found");
}
}
/**
* Check if option is available in SPFx for older versions
* @param {Array} options
* @param {boolean} legacy
*/
checkForLegacySupport(options, environment) {
let legacy = false;
if (environment === 'onprem') {
legacy = true;
}
return options.filter(item => {
if (item.legacySupport === undefined) {
return {
name: item.name,
value: item.value
}
}
if (legacy === true && item.legacySupport === true) {
return {
name: item.name,
value: item.value
};
}
if (legacy === false) {
return {
name: item.name,
value: item.value
}
}
})
}
/**
* Returns component name that last created
* @param {*} componentClassName
* @param {*} componentType
*/
getComponentName(componentClassName, componentType) {
let componentName = componentClassName;
if (componentType.toLowerCase() === 'listviewcommandset') {
componentName = componentName.slice(0, componentName.toLowerCase().lastIndexOf('commandset'));
} else {
componentName = componentName.slice(0, componentName.toLowerCase().lastIndexOf(componentType.toLowerCase()));
}
return componentName;
}
/**
* returns the current componentManifest of the project
* @param {*} yeoman
*/
getComponentManifest(yeoman) {
const yorcPath = yeoman.destinationPath('.yo-rc.json'),
spfxConfigPath = yeoman.destinationPath('./config/config.json'),
spfxNS = '@microsoft/generator-sharepoint',
pnpNS = '@pnp/generator-spfx',
spfxTemplateFolder = './spfx/';
if (!fs.existsSync(yorcPath)) {
return null;
}
let yoConfig = JSON.parse(
fs.readFileSync(yorcPath, 'utf-8')
);
if (yoConfig[pnpNS] === undefined) {
const pnpConfig = JSON.parse(
fs.readFileSync(`${path.dirname(yeoman.destinationPath())}/.yo-rc.json`, 'utf-8')
);
_.merge(yoConfig, pnpConfig);
}
if (yoConfig[spfxNS] === undefined) {
return null;
}
let spfxScope = yoConfig[spfxNS],
environment = spfxScope.environment,
componentType = spfxScope.componentType === 'webpart' ? spfxScope.componentType : spfxScope.extensionType.toLowerCase(),
templatePath = spfxTemplateFolder + componentType + '-' + environment + '/';
// Currently not supported for on-premises
if (environment === 'onprem') {
yeoman.log('On-Premises components are currently not supported to inject code automatically')
return null;
}
let spfxConfigContent =
fs.readFileSync(spfxConfigPath, 'utf-8')
let spfxConfig = JSON.parse(spfxConfigContent);
if (spfxConfig.bundles === undefined) {
yeoman.log('no bundles defined');
return null;
}
const bundles = Object.keys(
spfxConfig.bundles
);
let spfxItem = spfxConfig.bundles[
bundles[
bundles.length - 1
]
].components[0];
let manifestPath = spfxItem.manifest,
entrypoint = spfxItem.entrypoint,
entrySrc = entrypoint.replace(/\/lib\//g, '/src/')
.replace('.js', '.ts');
if (manifestPath === undefined) {
return null;
}
if (!fs.existsSync(yeoman.destinationPath(manifestPath))) {
return null;
}
// remove comments from malicous SPFx json
let manifestContent =
this._removeJsonComments(
fs.readFileSync(yeoman.destinationPath(manifestPath), 'utf-8')
);
// declare manifest content
let manifest;
try {
manifest = JSON.parse(manifestContent);
// Adding basefolder of component
manifest._componentPath = 'this.componentPath contains path to the component';
manifest.componentPath = path.dirname(entrySrc);
manifest._componentFile = 'this.componentFile contains path to the main file of component';
manifest.componentMainFile = entrySrc;
manifest._templatePath = 'this.templatePath contains path to find template of component';
manifest.templatePath = templatePath;
manifest._componentClassName = 'this.componentClassName contains path to real class name of the file';
manifest.componentClassName = path.basename(entrySrc, path.extname(entrySrc));
manifest._componentName = 'this.componentName contains class name as it was entered by user, without SPFx component type prefix (a.k.a WebPart, CommandSet, etc.)';
manifest.componentName = this.getComponentName(manifest.componentClassName, componentType);
} catch (error) {
yeoman.log(error);
return null;
}
return manifest;
}
/**
* Writes template files to the project folder
*/
_writeTemplateFile(yeoman, sourcePath, targetPath, ejsInject) {
// Reading Source FIle
let tmplContent = fs.readFileSync(sourcePath, 'utf-8');
let fileRelativePath = path.relative(yeoman.destinationPath(''), targetPath);
// Generate new Content
let generatedContent = ejs.render(tmplContent, ejsInject);
yeoman.log(chalk.green(' update'), fileRelativePath);
fs.writeFileSync(targetPath, generatedContent);
}
/**
* deploy templates
* @param {*} yeoman
* @param {*} injections
*/
deployTemplates(yeoman, injections) {
const component = this.getComponentManifest(yeoman);
if (component === null) {
return null;
}
const templatePath = yeoman.templatePath(component.templatePath);
const ejsInject = {
componentClassName: component.componentClassName,
componentClassNameKebabCase: paramCase(component.componentClassName),
componentNameCamelCase: component.componentClassName,
componentStrings: component.componentClassName + 'Strings',
componentPath: component.componentPath,
componentName: component.componentName
};
Object.assign(ejsInject, injections);
this.deployTemplatesToPath(yeoman, ejsInject, templatePath, component.componentPath);
}
/**
*
* @param {*} yeoman
* @param {*} injections
*/
updateReadmeFile(yeoman, injections) {
const readmePath = yeoman.destinationPath('README.md'),
templateReadmePath = yeoman.templatePath('../../../app/templates/README.md'),
pkg = require(yeoman.destinationPath('package.json'));
const templateReadme = fs.readFileSync(templateReadmePath, 'UTF-8');
injections.libraryName = pkg.name;
const newReadmeFile = ejs.render(templateReadme, injections);
fs.writeFileSync(readmePath, newReadmeFile, 'UTF-8');
}
/**
* Deploy templates to file
* @param {*} yeoman
* @param {*} injections
* @param {*} templatePath
* @param {*} targetDir
*/
deployTemplatesToPath(yeoman, injections, templatePath, targetDir) {
targetDir = targetDir
.replace('{componentClassName}', injections.componentClassName)
.replace('{componentClassNameKebabCase}', injections.componentClassNameKebabCase)
.replace('{componentName}', injections.componentName);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const files = fs.readdirSync(templatePath);
files.forEach(file => {
// Source Path
let sourcePath = yeoman.templatePath(
path.join(templatePath, file)
);
// Check if current 'file' is a directory
if (fs.lstatSync(sourcePath).isDirectory()) {
const targetPath = path.join(targetDir, path.basename(sourcePath));
this.deployTemplatesToPath(yeoman, injections, sourcePath, targetPath);
} else {
// Generate filename dynamically
let fileName = file
.replace('{componentClassName}', injections.componentClassName)
.replace('{componentClassNameKebabCase}', injections.componentClassNameKebabCase)
.replace('{componentName}', injections.componentName);
// generate target destination file path
let targetFile = yeoman.destinationPath(
path.join(targetDir, fileName)
);
this._writeTemplateFile(yeoman, sourcePath, targetFile, injections);
}
})
}
}
module.exports = new Util();