powerbi-visuals-tools 4.3.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/Changelog.md +7 -0
  2. package/README.md +1 -1
  3. package/bin/pbiviz.js +55 -36
  4. package/certs/PowerBICustomVisualTest_private.key +26 -26
  5. package/certs/PowerBICustomVisualTest_public.crt +17 -17
  6. package/config.json +27 -34
  7. package/lib/CertificateTools.js +119 -143
  8. package/lib/CommandManager.js +52 -0
  9. package/lib/ConsoleWriter.js +63 -85
  10. package/lib/TemplateFetcher.js +23 -30
  11. package/lib/VisualGenerator.js +42 -56
  12. package/lib/VisualManager.js +193 -0
  13. package/lib/WebPackWrap.js +96 -145
  14. package/lib/utils.js +21 -13
  15. package/lib/webpack.config.js +47 -56
  16. package/package.json +34 -26
  17. package/spec/clean-tests.js +1 -1
  18. package/spec/e2e/pbivizCertSpec.js +14 -13
  19. package/spec/e2e/pbivizInfoSpec.js +7 -10
  20. package/spec/e2e/pbivizNewSpec.js +53 -65
  21. package/spec/e2e/pbivizPackageSpec.js +86 -90
  22. package/spec/e2e/pbivizStartSpec.js +6 -7
  23. package/spec/e2e/pbivizWebpackVerSpec.js +14 -16
  24. package/spec/e2e/{utils.js → testUtils.js} +9 -12
  25. package/spec/helpers/FileSystem.js +18 -18
  26. package/spec/jasmine-runner.js +5 -5
  27. package/src/CertificateTools.ts +431 -0
  28. package/src/CommandManager.ts +78 -0
  29. package/src/ConsoleWriter.ts +206 -0
  30. package/src/TemplateFetcher.ts +122 -0
  31. package/src/VisualGenerator.ts +236 -0
  32. package/src/VisualManager.ts +220 -0
  33. package/src/WebPackWrap.ts +299 -0
  34. package/src/utils.ts +41 -0
  35. package/src/webpack.config.ts +144 -0
  36. package/templates/pbiviz-json-template.js +2 -2
  37. package/templates/pbiviz.json.template +1 -1
  38. package/templates/plugin-ts-template.js +1 -1
  39. package/templates/visuals/default/.eslintignore +5 -0
  40. package/templates/visuals/default/.eslintrc.js +20 -0
  41. package/templates/visuals/default/package.json +9 -8
  42. package/templates/visuals/default/pbiviz.json +3 -2
  43. package/templates/visuals/default/tsconfig.json +2 -2
  44. package/templates/visuals/rhtml/.eslintignore +5 -0
  45. package/templates/visuals/rhtml/.eslintrc.js +20 -0
  46. package/templates/visuals/rhtml/package.json +7 -6
  47. package/templates/visuals/rhtml/pbiviz.json +2 -1
  48. package/templates/visuals/rvisual/.eslintignore +5 -0
  49. package/templates/visuals/rvisual/.eslintrc.js +20 -0
  50. package/templates/visuals/rvisual/package.json +5 -4
  51. package/templates/visuals/slicer/.eslintignore +5 -0
  52. package/templates/visuals/slicer/.eslintrc.js +20 -0
  53. package/templates/visuals/slicer/package.json +8 -7
  54. package/templates/visuals/table/.eslintignore +5 -0
  55. package/templates/visuals/table/.eslintrc.js +20 -0
  56. package/templates/visuals/table/package.json +8 -7
  57. package/templates/visuals/table/tsconfig.json +4 -0
  58. package/tsconfig.json +22 -0
  59. package/bin/pbiviz-info.js +0 -54
  60. package/bin/pbiviz-new.js +0 -82
  61. package/bin/pbiviz-package.js +0 -122
  62. package/bin/pbiviz-start.js +0 -142
  63. package/lib/CommandHelpManager.js +0 -51
  64. package/lib/VisualPackage.js +0 -118
  65. package/templates/visuals/default/tslint.json +0 -9
  66. package/templates/visuals/rhtml/tslint.json +0 -9
  67. package/templates/visuals/rvisual/tslint.json +0 -9
  68. package/templates/visuals/slicer/tslint.json +0 -9
  69. package/templates/visuals/table/tslint.json +0 -9
@@ -0,0 +1,206 @@
1
+ /*
2
+ * Power BI Visual CLI
3
+ *
4
+ * Copyright (c) Microsoft Corporation
5
+ * All rights reserved.
6
+ * MIT License
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the ""Software""), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in
16
+ * all copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
+ * THE SOFTWARE.
25
+ */
26
+
27
+ "use strict";
28
+
29
+ import chalk from 'chalk';
30
+ import fs from 'fs';
31
+ import os from 'os';
32
+ import path from 'path';
33
+ import { getRootPath } from './utils.js';
34
+
35
+ const preferredChalk = os.platform() === 'darwin' ? chalk.bold : chalk;
36
+
37
+ function prependLogTag(tag, args) {
38
+ return [tag].concat(args);
39
+ }
40
+
41
+ export default class ConsoleWriter {
42
+ /** Causes the terminal to beep */
43
+ static beep() {
44
+ process.stdout.write("\x07");
45
+ }
46
+
47
+ /** Outputs a blank line */
48
+ static blank() {
49
+ console.info(preferredChalk.reset(' '));
50
+ }
51
+
52
+ /**
53
+ * Outputs arguments with the "done" tag / colors
54
+ *
55
+ * @param {array} arguments - arguments passed through to console.info
56
+ */
57
+ static done(args) {
58
+ const tag = preferredChalk.bgGreen(' done ');
59
+ console.info.apply(this, prependLogTag(tag, args));
60
+ }
61
+
62
+ /**
63
+ * Outputs arguments with the "info" tag / colors
64
+ *
65
+ * @param {array} args - arguments passed through to console.info
66
+ */
67
+ static info(args) {
68
+ const tag = preferredChalk.bgCyan(' info ');
69
+ console.info.apply(this, prependLogTag(tag, args));
70
+ }
71
+
72
+ /**
73
+ * Outputs arguments with the "warn" tag / colors
74
+ *
75
+ * @param {array} args - arguments passed through to console.warn
76
+ */
77
+ static warning(args) {
78
+ const tag = preferredChalk.bgYellow.black(' warn ');
79
+ console.warn.apply(this, prependLogTag(tag, args));
80
+ }
81
+
82
+ /**
83
+ * Outputs arguments with the "error" tag / colors
84
+ *
85
+ * @param {array} args - arguments passed through to console.error
86
+ */
87
+ static error(args) {
88
+ const tag = preferredChalk.bgRed(' error ');
89
+ console.error.apply(this, prependLogTag(tag, args));
90
+ }
91
+
92
+ /**
93
+ * Outputs an object as a table
94
+ *
95
+ * @param {string} data - object to output
96
+ * @param {number} [depthLimit=Infinity] - limit the number of levels to recurse
97
+ * @param {string} [keyPrefix=''] - text to prepend to each key
98
+ */
99
+ static infoTable(data, depthLimit?, keyPrefix?) {
100
+ if (!data) {
101
+ return;
102
+ }
103
+ const limit = typeof depthLimit === 'undefined' ? Infinity : depthLimit;
104
+ for (const key in data) {
105
+ const item = data[key];
106
+ const itemKey = (keyPrefix || '') + key;
107
+ if (limit > 1 && typeof item === 'object' && !Array.isArray(item)) {
108
+ ConsoleWriter.infoTable(item, limit - 1, itemKey + '.');
109
+ } else {
110
+ ConsoleWriter.infoTableRow(itemKey, item);
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Outputs a table row with even spaced keys
117
+ *
118
+ * @param {string} key - title of this row
119
+ * @param {string} value - value for this row
120
+ * @param {number} [keyWidth=30] - width used for padding of the key column
121
+ */
122
+ static infoTableRow(key, value, keyWidth?) {
123
+ const width = keyWidth || 30;
124
+ const padding = Math.max(0, width - key.length);
125
+ const paddedKey = preferredChalk.bold(key) + (new Array(padding)).join('.');
126
+ ConsoleWriter.info([paddedKey, value]);
127
+ }
128
+
129
+ /**
130
+ * Outputs formatted errors
131
+ *
132
+ * @param {Array<Error>} errors
133
+ */
134
+ static formattedErrors(errors) {
135
+ if (errors && Array.isArray(errors)) {
136
+ errors.forEach((error) => {
137
+ if (!error) {
138
+ return;
139
+ }
140
+ const tag = error.type ? preferredChalk.bold(error.type.toUpperCase()) : 'UNKNOWN';
141
+ const file = error.filename ? preferredChalk.bgWhite.black(` ${error.filename} `) + ':' : '';
142
+ const position = (error.line && error.column) ? preferredChalk.cyan(`(${error.line},${error.column})`) : '';
143
+ const message = error.message || '';
144
+ ConsoleWriter.error([tag, `${file} ${position} ${message}`]);
145
+ });
146
+ } else {
147
+ ConsoleWriter.error(['UNKNOWN', errors]);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Outputs ascii art of the PowerBI logo
153
+ */
154
+ static getLogoVisualization() {
155
+ return fs.readFileSync(path.join(getRootPath(), 'assets', 'logo.txt')).toString();
156
+ }
157
+
158
+ /**
159
+ * Outputs validation log from PBIVIZ package checking
160
+ */
161
+ static validationLog(log) {
162
+
163
+ // api/js/css/pkg
164
+ const filterChecks = (attrCB, propCB) => {
165
+ for (const checkname in log) {
166
+ if (checkname !== 'valid') {
167
+ const checkpoint = log[checkname];
168
+ ConsoleWriter[checkpoint.error.length ? 'info' : 'done'](checkpoint.check);
169
+ attrCB(checkpoint, propCB);
170
+ }
171
+ }
172
+ };
173
+
174
+ // error/message/ok
175
+ const filterCheckAttrs = (checkpoint, propCB) => {
176
+ for (const propName in checkpoint) {
177
+ if (propName !== 'message') {
178
+ const prop = checkpoint[propName];
179
+ if (typeof (prop) === 'object' && prop.length) {
180
+ propCB(prop, propName);
181
+ }
182
+ }
183
+ }
184
+ };
185
+
186
+ // col/line/text
187
+ const filterAttrProps = (props, propName) => {
188
+ props.forEach((opt) => {
189
+ const result = [];
190
+ for (const key in opt) {
191
+ result.push(opt[key]);
192
+ }
193
+ if (result.length) {
194
+ ConsoleWriter[propName === 'error' ? 'error' : 'warn'](result.join(' --> '));
195
+ }
196
+ });
197
+ };
198
+
199
+ filterChecks(filterCheckAttrs, filterAttrProps);
200
+
201
+ const type = log.valid ? 'done' : 'error';
202
+ const text = log.valid ? 'Valid package' : 'Invalid package';
203
+ ConsoleWriter.blank();
204
+ ConsoleWriter[type](text);
205
+ }
206
+ }
@@ -0,0 +1,122 @@
1
+
2
+ import { createFolder, download, readJsonFromRoot } from './utils.js';
3
+ import ConsoleWriter from './ConsoleWriter.js';
4
+ import JSZip from 'jszip';
5
+ import VisualGenerator from "./VisualGenerator.js";
6
+ import { exec } from 'child_process';
7
+ import fs from 'fs-extra';
8
+ import path from 'path';
9
+
10
+ const config = readJsonFromRoot('config.json');
11
+
12
+ export default class TemplateFetcher {
13
+ private templateName: string;
14
+ private visualName: string;
15
+ private folderName: string;
16
+ private apiVersion: string;
17
+
18
+ constructor(templateName: string, visualName: string, apiVersion: string) {
19
+ this.templateName = templateName;
20
+ this.visualName = visualName;
21
+ this.folderName = `${this.visualName}`;
22
+ this.apiVersion = apiVersion;
23
+ }
24
+
25
+ fetch() {
26
+ const folder = createFolder.call(this, this.folderName);
27
+ download.call(this, config.visualTemplates[this.templateName], path.join(folder, "template.zip"))
28
+ .then(this.extractFiles.bind(this))
29
+ .then(this.removeZipFile.bind(this))
30
+ .then(this.setVisualGUID.bind(this))
31
+ .then(this.setApiVersion.bind(this))
32
+ .then(this.runNpmInstall.bind(this))
33
+ .then(this.showNextSteps.bind(this));
34
+ }
35
+
36
+ async removeZipFile() {
37
+ const folder = path.join("./", this.folderName);
38
+ const fileName = path.join(folder, "template.zip");
39
+ await fs.unlink(`.${path.sep}${fileName}`, (err) => {
40
+ if (err) {
41
+ ConsoleWriter.warning(`.${path.sep}${fileName} was not deleted`);
42
+ }
43
+ });
44
+ }
45
+
46
+ async extractFiles(file) {
47
+ const filePath = path.join(process.cwd(), file.path);
48
+ const buffer = await fs.readFile(filePath);
49
+ const zip = await JSZip.loadAsync(buffer);
50
+
51
+ const filesList = Object.keys(zip.files);
52
+ for (const filename of filesList) {
53
+ if (filename[filename.length - 1] === "/") {
54
+ // generate folders for exclude parent folder
55
+ const dest = path.join(path.dirname(filePath), path.join(filename, ".."));
56
+ await fs.ensureDir(dest);
57
+ } else {
58
+ // write files into dirs for exclude parent folder
59
+ const dest = path.join(path.dirname(filePath), path.join(path.dirname(filename), "..", filename.split("/").pop() ?? ""));
60
+ const content = await zip.file(filename)?.async('nodebuffer');
61
+ await fs.writeFile(dest, content);
62
+ }
63
+ }
64
+ }
65
+
66
+ async setApiVersion() {
67
+ if (!this.apiVersion) {
68
+ return;
69
+ }
70
+ ConsoleWriter.info(`Set Visual API to ${this.apiVersion}`);
71
+ const packageJsonFile = path.join(process.cwd(), this.folderName, "package.json");
72
+ const packageJson = await fs.readJson(packageJsonFile);
73
+ if (packageJson.devDependencies && packageJson.devDependencies["powerbi-visuals-api"]) {
74
+ packageJson.devDependencies["powerbi-visuals-api"] = `~${this.apiVersion}`;
75
+ }
76
+ if (packageJson.dependencies && packageJson.dependencies["powerbi-visuals-api"]) {
77
+ packageJson.dependencies["powerbi-visuals-api"] = `~${this.apiVersion}`;
78
+ }
79
+ await fs.writeJSON(packageJsonFile, packageJson);
80
+ }
81
+
82
+ async setVisualGUID() {
83
+ const pbivizJsonFile = path.join(process.cwd(), this.folderName, "pbiviz.json");
84
+ const pbivizJson = await fs.readJson(pbivizJsonFile);
85
+ pbivizJson.visual.guid = this.visualName + VisualGenerator.generateVisualGuid();
86
+ await fs.writeJSON(pbivizJsonFile, pbivizJson);
87
+ }
88
+
89
+ runNpmInstall() {
90
+ return new Promise((resolve, reject) => {
91
+ ConsoleWriter.info("Installing packages...");
92
+ process.chdir(this.folderName);
93
+ // const { stdout, stderr } = await exec('ls');
94
+ const child = exec('npm install', (error, stdout, stderr) => {
95
+ if (error) {
96
+ ConsoleWriter.error(error.stack);
97
+ ConsoleWriter.error(`Error code: ${error.code}`);
98
+ ConsoleWriter.error(`Signal received: ${error.signal}`);
99
+ }
100
+ ConsoleWriter.warning(stderr);
101
+ ConsoleWriter.info(stdout);
102
+ resolve(true);
103
+ });
104
+ child.on("error", (er) => {
105
+ ConsoleWriter.error(er);
106
+ reject();
107
+ });
108
+ child.on("exit", (code) => {
109
+ if (code !== 0) {
110
+ ConsoleWriter.error(`npm install stopped with code ${code}`);
111
+ reject();
112
+ }
113
+ });
114
+ });
115
+ }
116
+
117
+ showNextSteps() {
118
+ ConsoleWriter.blank();
119
+ ConsoleWriter.info("Run `npm run start` to start visual development");
120
+ ConsoleWriter.info("Run `npm run package` to create visual package");
121
+ }
122
+ }
@@ -0,0 +1,236 @@
1
+ /*
2
+ * Power BI Visual CLI
3
+ *
4
+ * Copyright (c) Microsoft Corporation
5
+ * All rights reserved.
6
+ * MIT License
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the ""Software""), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in
16
+ * all copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
+ * THE SOFTWARE.
25
+ */
26
+
27
+ "use strict";
28
+
29
+ import crypto from 'crypto';
30
+ import { getRootPath, readJsonFromRoot } from './utils.js';
31
+ import { compareVersions } from "compare-versions";
32
+ import fs from 'fs-extra';
33
+ import lodashDefaults from 'lodash.defaults';
34
+ import path from 'path';
35
+ import template from '../templates/pbiviz-json-template.js';
36
+
37
+ const config = readJsonFromRoot('config.json');
38
+
39
+ const VISUAL_TEMPLATES_PATH = path.join(getRootPath(), config.templates.visuals);
40
+ const API_VERSION = config.generate.apiVersion;
41
+ const minAPIversion = config.constants.minAPIversion;
42
+
43
+ /**
44
+ * Generates the data for the visual
45
+ */
46
+ function generateVisualOptions(visualName, apiVersion) {
47
+ const name = generateVisualName(visualName);
48
+ return {
49
+ name: name,
50
+ displayName: visualName,
51
+ guid: name + VisualGenerator.generateVisualGuid(),
52
+ visualClassName: 'Visual',
53
+ apiVersion: apiVersion
54
+ };
55
+ }
56
+
57
+ /**
58
+ *
59
+ */
60
+ function generateVisualName(displayName) {
61
+ return displayName.replace(/(?:^\w|[A-Z]|\b\w|_|\s+)/g, (match, index) => {
62
+ if (/\s|_+/.test(match)) {
63
+ return "";
64
+ }
65
+ return index === 0 ? match.toLowerCase() : match.toUpperCase();
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Creates a default pbiviz.json config file
71
+ *
72
+ * @param {string} visualPath - path to the visual
73
+ * @param {Object} options - visual information for populating the pbiviz.json template
74
+ * @param {string} templateName - external js files
75
+ */
76
+ function createPbiVizJson(visualPath, options, templateName) {
77
+
78
+ // read the global template data
79
+ // and generate the actual file content
80
+ let data = template(options);
81
+
82
+ // write out the target file content
83
+ const targetPath = path.join(visualPath, 'pbiviz.json');
84
+ fs.writeFileSync(targetPath, data);
85
+
86
+ let templatePath = path.join(VISUAL_TEMPLATES_PATH, templateName);
87
+ templatePath = path.join(templatePath, 'pbiviz.json');
88
+ if (templateName && fileExists(templatePath)) {
89
+ //read the target file content
90
+ data = fs.readJsonSync(targetPath);
91
+
92
+ //override externalJS settings with those of the local template file
93
+ const templateData = fs.readJsonSync(templatePath);
94
+ for (const objKey of Object.keys(templateData)) {
95
+ data[objKey] = templateData[objKey];
96
+ }
97
+
98
+ // write out the target file content
99
+ fs.writeJsonSync(targetPath, data);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Checks if the specified file exists
105
+ *
106
+ * @param {string} file - path to the file
107
+ */
108
+ function fileExists(file) {
109
+ try {
110
+ fs.accessSync(file);
111
+ } catch (e) {
112
+ return false;
113
+ }
114
+ return true;
115
+ }
116
+
117
+ /**
118
+ * Copies the visual template directory
119
+ *
120
+ * @param {string} targetPath - file path to root of visual package
121
+ * @param {string} templateName - template to use for generating the visual
122
+ */
123
+ function copyVisualTemplate(targetPath, templateName) {
124
+ fs.copySync(path.join(VISUAL_TEMPLATES_PATH, '_global'), targetPath);
125
+ fs.copySync(path.join(VISUAL_TEMPLATES_PATH, templateName), targetPath);
126
+ }
127
+
128
+ /**
129
+ * Checks if the specified template is valid
130
+ *
131
+ * @param {string} templateName - template to use for generating the visual
132
+ */
133
+ function validTemplate(templateName) {
134
+ try {
135
+ fs.accessSync(path.join(VISUAL_TEMPLATES_PATH, templateName));
136
+ } catch (e) {
137
+ return false;
138
+ }
139
+ return true;
140
+ }
141
+
142
+ const defaultOptions = {
143
+ force: false,
144
+ template: 'default',
145
+ apiVersion: API_VERSION,
146
+ externalJS: []
147
+ };
148
+
149
+ export default class VisualGenerator {
150
+ /**
151
+ * Generates a new visual
152
+ *
153
+ * @param {string} targetPath - file path for creation of the new visual package
154
+ * @param {string} visualName - name of the new visual package
155
+ * @param {object} options - specify options for the visual generator
156
+ * @returns {Promise<string>} - promise resolves with the path to the newly created package
157
+ */
158
+ static generateVisual(targetPath, visualName, options): Promise<string> {
159
+ return new Promise((resolve, reject) => {
160
+ const buildOptions = lodashDefaults(options, defaultOptions);
161
+ if (!buildOptions.apiVersion || compareVersions(buildOptions.apiVersion, minAPIversion) === -1) {
162
+ return reject(new Error(`Can not generate a visual with an API below than ${minAPIversion}, current API is '${buildOptions.apiVersion}'.`));
163
+ }
164
+ const visualOptions = generateVisualOptions(visualName, buildOptions.apiVersion);
165
+ const validationResult = VisualGenerator.checkVisualName(visualOptions.name);
166
+ if (!visualOptions || !visualOptions.name || validationResult) {
167
+ return reject(new Error(validationResult || "Invalid visual name"));
168
+ }
169
+
170
+ if (!validTemplate(buildOptions.template)) {
171
+ return reject(new Error(`Invalid template "${buildOptions.template}"`));
172
+ }
173
+
174
+ const visualPath = path.join(targetPath, visualOptions.name);
175
+ fs.access(visualPath, err => {
176
+ if (!err && !buildOptions.force) {
177
+ return reject(new Error('This visual already exists. Use force to overwrite.'));
178
+ }
179
+ try {
180
+ if (!err && buildOptions.force) {
181
+ fs.removeSync(visualPath);
182
+ }
183
+ copyVisualTemplate(visualPath, buildOptions.template);
184
+ createPbiVizJson(visualPath, visualOptions, options.template);
185
+ resolve(visualPath);
186
+ } catch (e) {
187
+ reject(e);
188
+ }
189
+ });
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Generates a random GUID for your visual
195
+ */
196
+ static generateVisualGuid() {
197
+ return crypto.randomUUID().replace(/-/g, '').toUpperCase();
198
+ }
199
+
200
+ /**
201
+ * Check visual name
202
+ * Using https://github.com/mathiasbynens/mothereff.in/tree/master/js-properties
203
+ *
204
+ * @static
205
+ * @param {string} name Visual name
206
+ * @returns {string} error message
207
+ *
208
+ * @memberof VisualGenerator
209
+ */
210
+ static checkVisualName(name) {
211
+ const regexES3ReservedWord = /^(?:do|if|in|for|int|new|try|var|byte|case|char|else|enum|goto|long|null|this|true|void|with|break|catch|class|const|false|final|float|short|super|throw|while|delete|double|export|import|native|public|return|static|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|interface|protected|transient|implements|instanceof|synchronized)$/;
212
+ const regexNumber = /^(?![+-])([0-9\+\-\.]+)/; // eslint-disable-line no-useless-escape
213
+ const regexZeroWidth = /\u200c|\u200d/;
214
+ const regexpWrongSymbols = /^[a-zA-Z0-9]+$/;
215
+ const valueAsUnescapedString = name.replace(/\\u([a-fA-F0-9]{4})|\\u\{([0-9a-fA-F]{1,})\}/g, ($0, $1, $2) => {
216
+ const codePoint = parseInt($2 || $1, 16);
217
+ // If it’s a surrogate…
218
+ if (codePoint >= 0xD800 && codePoint <= 0xDFFF) {
219
+ // Return a character that is never valid in an identifier.
220
+ // This prevents the surrogate from pairing with another.
221
+ return '\0';
222
+ }
223
+ return String.fromCodePoint(codePoint);
224
+ });
225
+ if (regexNumber.test(name)) {
226
+ return `The visual name can't begin with a number digit`;
227
+ } else if (!regexpWrongSymbols.test(name)) {
228
+ return `The visual name can contain only letters and numbers`;
229
+ } else if (regexES3ReservedWord.test(valueAsUnescapedString)) {
230
+ return `The visual name cannot be equal to a reserved JavaScript keyword.
231
+ More information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords`;
232
+ } else if (regexZeroWidth.test(valueAsUnescapedString)) {
233
+ return `The visual name can't be empty`;
234
+ }
235
+ }
236
+ }