generator-office
Version:
Yeoman generator for creating Microsoft Office projects using any text editor.
440 lines • 27 kB
JavaScript
/*
* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
* See LICENSE in the project root for license information.
*/
import _ from 'lodash';
import chalk from 'chalk';
import * as defaults from "./defaults.js";
import { helperMethods } from './helpers/helperMethods.js';
import projectsJsonData from './config/projectsJsonData.js';
import * as usageData from "office-addin-usage-data";
import { v4 as uuidv4 } from 'uuid';
import yosay from 'yosay';
import Generator from 'yeoman-generator';
import * as fs from "fs";
import * as path from "path";
const excelCustomFunctions = `excel-functions`;
let isSsoProject = false;
const javascript = `JavaScript`;
let language;
const manifest = 'manifest';
const sso = 'single-sign-on';
const typescript = `TypeScript`;
let jsonData;
let usageDataObject;
const usageDataOptions = {
groupName: usageData.groupName,
projectName: defaults.usageDataProjectName,
raisePrompt: false,
instrumentationKey: usageData.instrumentationKeyForOfficeAddinCLITools,
promptQuestion: defaults.usageDataPromptMessage,
usageDataLevel: usageData.UsageDataLevel.off,
method: usageData.UsageDataReportingMethod.applicationInsights,
isForTesting: false
};
export default class extends Generator {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
project;
/* Setup the generator */
constructor(args, opts) {
super(args, opts);
if (parseInt(process.version.slice(1, process.version.indexOf('.'))) % 2 == 1) {
console.log(yosay('generator-office does not support your version of Node. Please switch to the latest LTS version of Node.'));
this._exitProcess();
}
this.argument('projectType', { type: String, required: false });
this.argument('name', { type: String, required: false });
this.argument('host', { type: String, required: false });
this.argument('manifestType', { type: String, required: false });
this.option('skip-install', {
type: Boolean,
description: 'Skip running `npm install` post scaffolding.'
});
this.option('js', {
type: Boolean,
description: 'Project uses JavaScript instead of TypeScript.'
});
this.option('ts', {
type: Boolean,
description: 'Project uses TypeScript instead of JavaScript.'
});
this.option('output', {
alias: 'o',
type: String,
description: 'Project folder name if different from project name.'
});
this.option('prerelease', {
type: String,
description: 'Use the prerelease version of the project template.'
});
this.option('test', {
type: String,
description: 'Project is created in the context of unit tests.'
});
this.option('details', {
alias: 'd',
type: Boolean,
description: 'Get more details on Yo Office arguments.'
});
}
/* Generator initalization */
async initializing() {
if (this.options["details"]) {
await this._detailedHelp();
}
if (this.options['test']) {
usageDataOptions.isForTesting = true;
}
if (this.options['skip-install']) {
this.options.skipInstall = true;
}
const message = `Welcome to the ${chalk.bold.green('Office Add-in')} generator, by ${chalk.bold.green('@OfficeDev')}! Let\'s create a project together!`;
await this.log(yosay(message));
jsonData = new projectsJsonData(this.templatePath());
}
/* Prompt user for project options */
async prompting() {
try {
if (usageData.needToPromptForUsageData(usageDataOptions.groupName || "")) {
const promptForUsageData = [
{
name: 'usageDataPromptAnswer',
message: usageDataOptions.promptQuestion || defaults.usageDataPromptMessage,
type: 'list',
default: 'Continue',
choices: ['Continue', 'Exit'],
}
];
const answerForUsageDataPrompt = await this.prompt(promptForUsageData);
if (answerForUsageDataPrompt?.usageDataPromptAnswer === 'Continue') {
usageDataOptions.usageDataLevel = usageData.UsageDataLevel.on;
}
else {
process.exit();
}
}
else {
usageDataOptions.usageDataLevel = usageData.readUsageDataLevel(usageDataOptions.groupName || "");
}
let isManifestProject = false;
let isExcelFunctionsProject = false;
// Normalize host name if passed as a command line argument
if (this.options["host"] != null) {
this.options["host"] = jsonData.getHostDisplayName(this.options["host"]);
}
/* askForProjectType will only be triggered if no project type was specified via command line projectType argument,
* and the projectType argument input was indeed valid */
const startForProjectType = (new Date()).getTime();
const askForProjectType = [
{
name: 'projectType',
message: 'Choose a project type:',
type: 'list',
default: 'React',
choices: jsonData.getProjectTemplateNames().map(template => ({ name: jsonData.getProjectDisplayName(template), value: template })),
when: this.options["projectType"] == null || !jsonData.isValidProjectType(this.options["projectType"])
}
];
const answerForProjectType = await this.prompt(askForProjectType);
const endForProjectType = (new Date()).getTime();
const durationForProjectType = (endForProjectType - startForProjectType) / 1000;
const projectType = _.toLower(this.options["projectType"]) || _.toLower(answerForProjectType.projectType);
/* Set isManifestProject to true if Manifest project type selected from prompt or Manifest was specified via the command prompt */
if ((answerForProjectType.projectType != null && _.toLower(answerForProjectType.projectType) === manifest)
|| (this.options["projectType"] != null && _.toLower(this.options["projectType"])) === manifest) {
isManifestProject = true;
}
/* Set isExcelFunctionsProject to true if ExcelFunctions project type selected from prompt or Excel Functions was specified via the command prompt */
if ((answerForProjectType.projectType != null && answerForProjectType.projectType) === excelCustomFunctions
|| (this.options["projectType"] != null && _.toLower(this.options["projectType"]) === excelCustomFunctions)) {
isExcelFunctionsProject = true;
}
/* Set isSsoProject to true if SSO project type selected from prompt or Single Sign-On was specified via the command prompt */
if ((answerForProjectType.projectType != null && answerForProjectType.projectType) === sso
|| (this.options["projectType"] != null && _.toLower(this.options["projectType"]) === sso)) {
isSsoProject = true;
}
const getSupportedScriptTypes = jsonData.getScriptTypeOptions(projectType);
const askForScriptType = [
{
name: 'scriptType',
type: 'list',
message: 'Choose a script type:',
choices: getSupportedScriptTypes,
default: getSupportedScriptTypes[0],
when: !this.options["js"] && !this.options["ts"] && !isManifestProject && getSupportedScriptTypes.length > 1
}
];
const answerForScriptType = await this.prompt(askForScriptType);
/* askforName will be triggered if no project name was specified via command line Name argument */
const askForName = [{
name: 'name',
type: 'input',
message: 'What do you want to name your add-in?',
default: 'My Office Add-in',
when: this.options["name"] == null
}];
const answerForName = await this.prompt(askForName);
/* askForHost will be triggered if no project name was specified via the command line Host argument, and the Host argument
* input was in fact valid, and the project type is not Excel-Functions */
const startForHost = (new Date()).getTime();
const supportedHosts = jsonData.getHostOptions(projectType);
const askForHost = [{
name: 'host',
message: 'Which Office client application would you like to support?',
type: 'list',
default: supportedHosts[0],
choices: supportedHosts.map(host => ({ name: host, value: host })),
when: (this.options["host"] == null || this.options["host"] != null && !jsonData.isValidHost(this.options["host"]))
&& supportedHosts.length > 1
}];
const answerForHost = await this.prompt(askForHost);
const endForHost = (new Date()).getTime();
const durationForHost = (endForHost - startForHost) / 1000;
const selectedHost = this.options["host"] || answerForHost.host || supportedHosts[0];
usageDataObject = new usageData.OfficeAddinUsageData(usageDataOptions);
/* aksForManifestType will be triggered if no type was specified via the command line manifestType argument */
const startForManifestType = (new Date()).getTime();
const manifestOptions = jsonData.getManifestOptions(projectType, selectedHost);
const askForManifestType = [{
name: 'manifestType',
message: 'Which manifest type would you like to use?',
type: 'list',
default: manifestOptions[0],
choices: manifestOptions.map(manifestType => ({ name: jsonData.getManifestDisplayName(manifestType), value: manifestType })),
when: (this.options["manifestType"] == null || this.options["manifestType"] != null && !jsonData.isValidManifestType(this.options["manifestType"]))
&& jsonData.getManifestOptions(projectType, selectedHost).length > 1
}];
const answerForManifestType = await this.prompt(askForManifestType);
const endForManifestType = (new Date()).getTime();
const durationForManifestType = (endForManifestType - startForManifestType) / 1000;
usageDataObject = new usageData.OfficeAddinUsageData(usageDataOptions);
/* Configure project properties based on user input or answers to prompts */
this._configureProject(answerForProjectType, answerForManifestType, answerForScriptType, answerForHost, answerForName, isManifestProject, isExcelFunctionsProject);
const projectInfo = {
Host: [this.project.host, durationForHost],
ScriptType: [this.project.scriptType],
IsManifestOnly: [this.project.isManifestOnly.toString()],
ProjectType: [this.project.projectType, durationForProjectType],
ManifestType: [this.project.manifestType, durationForManifestType],
isForTesting: [usageDataOptions.isForTesting]
};
// Send usage data for project created
usageDataObject.reportEvent(defaults.promptSelectionstEventName, projectInfo);
}
catch (err) {
usageDataObject = new usageData.OfficeAddinUsageData(usageDataOptions);
usageDataObject.reportError(defaults.promptSelectionsErrorEventName, new Error('Prompting Error: ' + err));
}
}
async writing() {
await this._copyProjectFiles()
.catch((err) => {
usageDataObject.reportError(defaults.copyFilesErrorEventName, new Error('Installation Error: ' + err));
process.exitCode = 1;
});
}
async install() {
// Call 'convert-to-single-host' npm script in generated project, passing in host parameter
// Need to call this here after package.json was written to disk, but before npm install is called
await this.spawn("npm", ["run", "convert-to-single-host", "--if-present", "--", _.toLower(this.project.hostInternalName), this.project.manifestType, this.project.name]);
}
async end() {
if (!this.options['test']) {
try {
await this._postInstallHints();
}
catch (err) {
usageDataObject.reportError(defaults.postInstallHintsErrorEventName, new Error('Exit Error: ' + err));
}
}
}
_configureProject(answerForProjectType, answerForManifestType, answerForScriptType, answerForHost, answerForName, isManifestProject, isExcelFunctionsProject) {
try {
const projType = _.toLower(this.options["projectType"]) || _.toLower(answerForProjectType.projectType);
const supportedHosts = jsonData.getHostOptions(projType);
const selectedHost = this.options["host"] || answerForHost.host || supportedHosts[0];
this.project = {
folder: this.options["output"] || answerForName.name || this.options["name"],
host: answerForHost.host
? answerForHost.host
: this.options["host"]
? this.options["host"]
: jsonData?.getHostOptions(projType)[0],
manifestType: answerForManifestType.manifestType
? answerForManifestType.manifestType
: this.options["manifestType"]
? this.options["manifestType"]
: jsonData?.getManifestOptions(projType, selectedHost)[0],
name: this.options["name"] || answerForName.name,
projectType: projType,
scriptType: answerForScriptType.scriptType
? answerForScriptType.scriptType
: this.options["ts"]
? typescript
: this.options["js"]
? javascript
: jsonData?.getScriptTypeOptions(projType)[0],
isManifestOnly: isManifestProject,
isExcelFunctionsProject: isExcelFunctionsProject,
};
/* Set folder if to output param if specified */
if (this.options["output"] != null) {
this.project.folder = this.options["output"];
}
/* Set language variable */
language = this.project.scriptType === typescript ? 'ts' : 'js';
this.project.projectInternalName = _.kebabCase(this.project.name);
this.project.projectDisplayName = this.project.name;
this.project.projectId = uuidv4();
this.project.hostInternalName = this.project.host == "All" ? "wxpo" : this.project.host;
this.destinationRoot(this.project.folder);
process.chdir(this.destinationRoot());
this.env.cwd = this.destinationRoot();
/* Check to to see if destination folder already exists. If so, we will exit and prompt the user to provide
a different project name or output folder */
this._exitYoOfficeIfProjectFolderExists();
}
catch (err) {
usageDataObject.reportError(defaults.configurationErrorEventName, new Error('Configuration Error: ' + err));
}
}
async _copyProjectFiles() {
return new Promise(async (resolve, reject) => {
try {
const projectRepoBranchInfo = jsonData.getProjectRepoAndBranch(this.project.projectType, language, this.options["prerelease"]);
this._projectCreationMessage();
// Copy project template files from project repository (currently only custom functions has its own separate repo)
if (projectRepoBranchInfo.repo && projectRepoBranchInfo.branch) {
const projectFolder = this.destinationPath();
const zipFile = await helperMethods.downloadProjectTemplateZipFile(projectFolder, projectRepoBranchInfo.repo, projectRepoBranchInfo.branch);
const unzippedFolder = await helperMethods.unzipProjectTemplate(projectFolder);
const moveFromFolder = this.destinationPath(unzippedFolder);
// delete original zip file
if (fs.existsSync(zipFile)) {
fs.unlinkSync(zipFile);
}
// loop through all the files and folders in the unzipped folder and move them to project root
fs.readdirSync(moveFromFolder)
.filter((file) => !file.includes(".gitignore") && !file.includes("package.json"))
.forEach(function (file) {
const fromPath = path.join(moveFromFolder, file);
const toPath = path.join(projectFolder, file);
fs.renameSync(fromPath, toPath);
});
// copy package.json file to new project directory and trigger npm install
this.fs.copyTpl(path.join(moveFromFolder, "package.json"), path.join(projectFolder, "package.json"));
// delete project zipped folder
helperMethods.deleteFolderRecursively(this.destinationPath(unzippedFolder));
}
else {
// Manifest-only project
const templateFills = Object.assign({}, this.project);
this.fs.copyTpl(this.templatePath(`hosts/${_.toLower(this.project.hostInternalName)}/manifest.xml`), this.destinationPath('manifest.xml'), templateFills);
this.fs.copyTpl(this.templatePath(`manifest-only/**`), this.destinationPath(), templateFills);
}
return resolve();
}
catch (err) {
usageDataObject.reportError(defaults.copyFilesErrorEventName, new Error("File Copy Error: " + err));
return reject(err);
}
});
}
async _postInstallHints() {
const projFolder = /\s/.test(this.destinationRoot()) ? "\"" + this.destinationRoot() + "\"" : this.destinationRoot();
let stepNumber = 1;
/* Next steps and npm commands */
await this.log('----------------------------------------------------------------------------------------------------------\n');
await this.log(` ${chalk.green('Congratulations!')} Your add-in has been created! Your next steps:\n`);
await this.log(` ${stepNumber++}. Go the directory where your project was created:\n`);
await this.log(` ${chalk.bold('cd ' + projFolder)}\n`);
if (isSsoProject) {
await this.log(` ${stepNumber++}. Configure your SSO taskpane add-in:\n`);
await this.log(` ${chalk.bold('npm run configure-sso')}\n`);
}
else if (this.project.isExcelFunctionsProject) {
await this.log(` ${stepNumber++}. Build your Excel Custom Functions taskpane add-in:\n`);
await this.log(` ${chalk.bold('npm run build')}\n`);
}
if (!this.project.isManifestOnly) {
if (this.project.host === "Excel" || this.project.host === "Word" || this.project.host === "Powerpoint" || this.project.host === "Outlook" || this.project.host === "All") {
await this.log(` ${stepNumber++}. Start the local web server and sideload the add-in:\n`);
await this.log(` ${chalk.bold('npm start')}\n`);
}
else {
await this.log(` ${stepNumber++}. Start the local web server:\n`);
await this.log(` ${chalk.bold('npm run dev-server')}\n`);
await this.log(` ${stepNumber++}. Sideload the the add-in:\n`);
await this.log(` ${chalk.bold('Follow these instructions:')}`);
await this.log(` ${defaults.networkShareSideloadingSteps}\n`);
}
}
await this.log(` ${stepNumber++}. Open the project in VS Code:\n`);
await this.log(` ${chalk.bold('code .')}\n`);
await this.log(` For more information, visit http://code.visualstudio.com.\n`);
await this.log(` Please visit https://learn.microsoft.com/office/dev/add-ins for more information about Office Add-ins.\n`);
if (this.project.host === "Outlook") {
await this.log(` Please visit ${defaults.outlookSideloadingSteps} for more information about Outlook sideloading.\n`);
}
await this.log('----------------------------------------------------------------------------------------------------------\n');
this._exitProcess();
}
async _projectCreationMessage() {
/* Log to console the type of project being created */
if (this.project.isManifestOnly) {
await this.log('----------------------------------------------------------------------------------\n');
await this.log(` Creating manifest for ${chalk.bold.green(this.project.projectDisplayName)} at ${chalk.bold.magenta(this.destinationRoot())}\n`);
await this.log('----------------------------------------------------------------------------------');
}
else {
await this.log('\n----------------------------------------------------------------------------------\n');
await this.log(` Creating ${chalk.bold.green(this.project.projectDisplayName)} add-in for ${chalk.bold.magenta(_.capitalize(this.project.host))}`);
await this.log(` using ${chalk.bold.yellow(this.project.scriptType)} and ${chalk.bold.magenta(jsonData.getProjectDisplayName(this.project.projectType))} and ${chalk.bold.yellow(jsonData.getManifestDisplayName(this.project.manifestType))}`);
await this.log(` at ${chalk.bold.magenta(this.destinationRoot())}\n`);
await this.log('----------------------------------------------------------------------------------');
}
}
async _detailedHelp() {
await this.log(`\nYo Office ${chalk.bgGreen('Arguments')} and ${chalk.bgMagenta('Options.')}\n`);
await this.log(`NOTE: ${chalk.bgGreen('Arguments')} must be specified in the order below, and ${chalk.bgMagenta('Options')} must follow ${chalk.bgGreen('Arguments')}.\n`);
await this.log(` ${chalk.bgGreen('projectType')}:Specifies the type of project to create. Valid project types include:`);
await this.log(` ${chalk.yellow('taskpane:')} Creates an 'Office Add-in Task Pane project' project.`);
await this.log(` ${chalk.yellow('react:')} Creates an 'Office add-in using React framework' project.`);
await this.log(` ${chalk.yellow('excel-functions-shared:')} Creates an 'Office add-in for Excel custom functions using a Shared Runtime' project.`);
await this.log(` ${chalk.yellow('excel-functions:')} Creates an 'Office add-in for Excel custom functions using a JavaScript-only Runtime' project.`);
await this.log(` ${chalk.yellow('single-sign-on:')} Creates an 'Office Add-in Task Pane project supporting single sign-on' project.`);
await this.log(` ${chalk.yellow('nested-app-auth:')} Creates an 'Office Add-in Task Pane project supporting Nested App Auth single sign-on (preview)' project.`);
await this.log(` ${chalk.yellow('manifest:')} Creates an only the manifest file for an Office add-in project.\n`);
await this.log(` ${chalk.bgGreen('name')}:Specifies the name for the project that will be created.\n`);
await this.log(` ${chalk.bgGreen('host')}:Specifies the host app in the add-in manifest. Valid hosts include:`);
await this.log(` ${chalk.yellow('excel:')} Creates an Office add-in for Excel.`);
await this.log(` ${chalk.yellow('onenote:')} Creates an Office add-in for OneNote.`);
await this.log(` ${chalk.yellow('outlook:')} Creates an Office add-in for Outlook.`);
await this.log(` ${chalk.yellow('powerpoint:')} Creates an Office add-in for PowerPoint.`);
await this.log(` ${chalk.yellow('project:')} Creates an Office add-in for Project.`);
await this.log(` ${chalk.yellow('word:')} Creates an Office add-in for Word.\n`);
await this.log(` ${chalk.bgGreen('manifestType')}:Specifies the manifest type to use for the add-in. Valid types include:`);
await this.log(` ${chalk.yellow('xml:')} Creates a Add-in only manifest`);
await this.log(` ${chalk.yellow('json:')} Creates a unified manifest for Microsoft 365.\n`);
await this.log(` ${chalk.bgMagenta('--output')}:Specifies the location in the file system where the project will be created.`);
await this.log(` ${chalk.yellow('If the option is not specified, the project will be created in the current folder')}\n`);
await this.log(` ${chalk.bgMagenta('--js')}:Specifies that the project will use JavaScript instead of TypeScript.`);
await this.log(` ${chalk.yellow('If the option is not specified, Yo Office will prompt for TypeScript or JavaScript')}\n`);
await this.log(` ${chalk.bgMagenta('--ts')}:Specifies that the project will use TypeScript instead of JavaScript.`);
await this.log(` ${chalk.yellow('If the option is not specified, Yo Office will prompt for TypeScript or JavaScript')}\n`);
this._exitProcess();
}
async _exitYoOfficeIfProjectFolderExists() {
if (helperMethods.doesProjectFolderExist(this.destinationRoot())) {
await this.log(`${chalk.bold.red(`\nFolder already exists at ${chalk.bold.green(this.destinationRoot())} and is not empty. To avoid accidentally overwriting any files, please start over and choose a different project name or destination folder via the ${chalk.bold.magenta(`--output`)} parameter`)}\n`);
this._exitProcess();
}
return false;
}
_exitProcess() {
process.exit();
}
}
;
//# sourceMappingURL=index.js.map