powerbi-visuals-tools 7.0.0 → 7.0.1
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.
- package/Changelog.md +4 -0
- package/certs/blank +0 -0
- package/lib/CertificateTools.js +262 -0
- package/lib/CommandManager.js +75 -0
- package/lib/ConsoleWriter.js +188 -0
- package/lib/FeatureManager.js +43 -0
- package/lib/LintValidator.js +61 -0
- package/lib/Package.js +46 -0
- package/lib/TemplateFetcher.js +111 -0
- package/lib/Visual.js +29 -0
- package/lib/VisualGenerator.js +221 -0
- package/lib/VisualManager.js +316 -0
- package/lib/WebPackWrap.js +274 -0
- package/lib/features/APIVersion.js +16 -0
- package/lib/features/AdvancedEditMode.js +12 -0
- package/lib/features/AllowInteractions.js +12 -0
- package/lib/features/AnalyticsPane.js +17 -0
- package/lib/features/BaseFeature.js +9 -0
- package/lib/features/Bookmarks.js +12 -0
- package/lib/features/ColorPalette.js +12 -0
- package/lib/features/ConditionalFormatting.js +12 -0
- package/lib/features/ContextMenu.js +12 -0
- package/lib/features/DrillDown.js +16 -0
- package/lib/features/FeatureTypes.js +23 -0
- package/lib/features/FetchMoreData.js +22 -0
- package/lib/features/FileDownload.js +12 -0
- package/lib/features/FormatPane.js +12 -0
- package/lib/features/HighContrast.js +12 -0
- package/lib/features/HighlightData.js +12 -0
- package/lib/features/KeyboardNavigation.js +12 -0
- package/lib/features/LandingPage.js +12 -0
- package/lib/features/LaunchURL.js +12 -0
- package/lib/features/LocalStorage.js +12 -0
- package/lib/features/Localizations.js +12 -0
- package/lib/features/ModalDialog.js +12 -0
- package/lib/features/RenderingEvents.js +13 -0
- package/lib/features/SelectionAcrossVisuals.js +12 -0
- package/lib/features/SyncSlicer.js +12 -0
- package/lib/features/Tooltips.js +12 -0
- package/lib/features/TotalSubTotal.js +12 -0
- package/lib/features/VisualVersion.js +12 -0
- package/lib/features/WarningIcon.js +12 -0
- package/lib/features/index.js +28 -0
- package/lib/utils.js +43 -0
- package/lib/webpack.config.js +169 -0
- package/package.json +7 -6
- package/.eslintrc.json +0 -21
- package/CVClientLA.md +0 -85
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createFolder, download, readJsonFromRoot } from './utils.js';
|
|
2
|
+
import ConsoleWriter from './ConsoleWriter.js';
|
|
3
|
+
import JSZip from 'jszip';
|
|
4
|
+
import VisualGenerator from "./VisualGenerator.js";
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
const config = await readJsonFromRoot('config.json');
|
|
9
|
+
export default class TemplateFetcher {
|
|
10
|
+
templateName;
|
|
11
|
+
visualName;
|
|
12
|
+
folderName;
|
|
13
|
+
apiVersion;
|
|
14
|
+
constructor(templateName, visualName, apiVersion) {
|
|
15
|
+
this.templateName = templateName;
|
|
16
|
+
this.visualName = visualName;
|
|
17
|
+
this.folderName = `${this.visualName}`;
|
|
18
|
+
this.apiVersion = apiVersion;
|
|
19
|
+
}
|
|
20
|
+
fetch() {
|
|
21
|
+
const folder = createFolder.call(this, this.folderName);
|
|
22
|
+
download.call(this, config.visualTemplates[this.templateName], path.join(folder, "template.zip"))
|
|
23
|
+
.then(this.extractFiles.bind(this))
|
|
24
|
+
.then(this.removeZipFile.bind(this))
|
|
25
|
+
.then(this.setVisualGUID.bind(this))
|
|
26
|
+
.then(this.setApiVersion.bind(this))
|
|
27
|
+
.then(this.runNpmInstall.bind(this))
|
|
28
|
+
.then(this.showNextSteps.bind(this));
|
|
29
|
+
}
|
|
30
|
+
async removeZipFile() {
|
|
31
|
+
const folder = path.join("./", this.folderName);
|
|
32
|
+
const fileName = path.join(folder, "template.zip");
|
|
33
|
+
await fs.unlink(`.${path.sep}${fileName}`, (err) => {
|
|
34
|
+
if (err) {
|
|
35
|
+
ConsoleWriter.warning(`.${path.sep}${fileName} was not deleted`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async extractFiles(file) {
|
|
40
|
+
const filePath = path.join(process.cwd(), file.path);
|
|
41
|
+
const buffer = await fs.readFile(filePath);
|
|
42
|
+
const zip = await JSZip.loadAsync(buffer);
|
|
43
|
+
const filesList = Object.keys(zip.files);
|
|
44
|
+
for (const filename of filesList) {
|
|
45
|
+
if (filename[filename.length - 1] === "/") {
|
|
46
|
+
// generate folders for exclude parent folder
|
|
47
|
+
const dest = path.join(path.dirname(filePath), path.join(filename, ".."));
|
|
48
|
+
await fs.ensureDir(dest);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// write files into dirs for exclude parent folder
|
|
52
|
+
const dest = path.join(path.dirname(filePath), path.join(path.dirname(filename), "..", filename.split("/").pop() ?? ""));
|
|
53
|
+
const content = await zip.file(filename)?.async('nodebuffer');
|
|
54
|
+
await fs.writeFile(dest, content);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async setApiVersion() {
|
|
59
|
+
if (!this.apiVersion) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
ConsoleWriter.info(`Set Visual API to ${this.apiVersion}`);
|
|
63
|
+
const packageJsonFile = path.join(process.cwd(), this.folderName, "package.json");
|
|
64
|
+
const packageJson = await fs.readJson(packageJsonFile);
|
|
65
|
+
if (packageJson.devDependencies && packageJson.devDependencies["powerbi-visuals-api"]) {
|
|
66
|
+
packageJson.devDependencies["powerbi-visuals-api"] = `~${this.apiVersion}`;
|
|
67
|
+
}
|
|
68
|
+
if (packageJson.dependencies && packageJson.dependencies["powerbi-visuals-api"]) {
|
|
69
|
+
packageJson.dependencies["powerbi-visuals-api"] = `~${this.apiVersion}`;
|
|
70
|
+
}
|
|
71
|
+
await fs.writeJSON(packageJsonFile, packageJson);
|
|
72
|
+
}
|
|
73
|
+
async setVisualGUID() {
|
|
74
|
+
const pbivizJsonFile = path.join(process.cwd(), this.folderName, "pbiviz.json");
|
|
75
|
+
const pbivizJson = await fs.readJson(pbivizJsonFile);
|
|
76
|
+
pbivizJson.visual.guid = this.visualName + VisualGenerator.generateVisualGuid();
|
|
77
|
+
await fs.writeJSON(pbivizJsonFile, pbivizJson);
|
|
78
|
+
}
|
|
79
|
+
runNpmInstall() {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
ConsoleWriter.info("Installing packages...");
|
|
82
|
+
process.chdir(this.folderName);
|
|
83
|
+
// const { stdout, stderr } = await exec('ls');
|
|
84
|
+
const child = exec('npm install', (error, stdout, stderr) => {
|
|
85
|
+
if (error) {
|
|
86
|
+
ConsoleWriter.error(error.stack);
|
|
87
|
+
ConsoleWriter.error(`Error code: ${error.code}`);
|
|
88
|
+
ConsoleWriter.error(`Signal received: ${error.signal}`);
|
|
89
|
+
}
|
|
90
|
+
ConsoleWriter.warning(stderr);
|
|
91
|
+
ConsoleWriter.info(stdout);
|
|
92
|
+
resolve(true);
|
|
93
|
+
});
|
|
94
|
+
child.on("error", (er) => {
|
|
95
|
+
ConsoleWriter.error(er);
|
|
96
|
+
reject();
|
|
97
|
+
});
|
|
98
|
+
child.on("exit", (code) => {
|
|
99
|
+
if (code !== 0) {
|
|
100
|
+
ConsoleWriter.error(`npm install stopped with code ${code}`);
|
|
101
|
+
reject();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
showNextSteps() {
|
|
107
|
+
ConsoleWriter.blank();
|
|
108
|
+
ConsoleWriter.info("Run `npm run start` to start visual development");
|
|
109
|
+
ConsoleWriter.info("Run `npm run package` to create visual package");
|
|
110
|
+
}
|
|
111
|
+
}
|
package/lib/Visual.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { compareVersions } from "compare-versions";
|
|
2
|
+
import { VisualFeatureType } from "./features/FeatureTypes.js";
|
|
3
|
+
export class Visual {
|
|
4
|
+
visualFeatureType;
|
|
5
|
+
capabilities;
|
|
6
|
+
config;
|
|
7
|
+
visualVersion;
|
|
8
|
+
constructor(capabilities, config) {
|
|
9
|
+
this.capabilities = capabilities;
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.visualFeatureType = this.getVisualFeatureType();
|
|
12
|
+
this.visualVersion = config.visual.version;
|
|
13
|
+
}
|
|
14
|
+
doesAPIVersionMatch(minAPIversion) {
|
|
15
|
+
return compareVersions(this.config.apiVersion ?? minAPIversion, minAPIversion) !== -1;
|
|
16
|
+
}
|
|
17
|
+
isVisualVersionValid(length) {
|
|
18
|
+
return this.visualVersion.split(".").length === length;
|
|
19
|
+
}
|
|
20
|
+
getVisualFeatureType() {
|
|
21
|
+
const isMatrixSupported = this.capabilities?.dataViewMappings?.some(dataView => dataView.matrix);
|
|
22
|
+
const isSlicer = Boolean(this.capabilities?.objects?.general?.properties?.filter?.type?.filter);
|
|
23
|
+
let type = isSlicer ? VisualFeatureType.Slicer : VisualFeatureType.NonSlicer;
|
|
24
|
+
if (isMatrixSupported) {
|
|
25
|
+
type = type | VisualFeatureType.Matrix;
|
|
26
|
+
}
|
|
27
|
+
return type;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
"use strict";
|
|
27
|
+
import crypto from 'crypto';
|
|
28
|
+
import { getRootPath, readJsonFromRoot } from './utils.js';
|
|
29
|
+
import { compareVersions } from "compare-versions";
|
|
30
|
+
import fs from 'fs-extra';
|
|
31
|
+
import lodashDefaults from 'lodash.defaults';
|
|
32
|
+
import path from 'path';
|
|
33
|
+
import template from '../templates/pbiviz-json-template.js';
|
|
34
|
+
const config = await readJsonFromRoot('config.json');
|
|
35
|
+
const VISUAL_TEMPLATES_PATH = path.join(getRootPath(), config.templates.visuals);
|
|
36
|
+
const API_VERSION = config.generate.apiVersion;
|
|
37
|
+
const minAPIversion = config.constants.minAPIversion;
|
|
38
|
+
/**
|
|
39
|
+
* Generates the data for the visual
|
|
40
|
+
*/
|
|
41
|
+
function generateVisualOptions(visualName, apiVersion) {
|
|
42
|
+
const name = generateVisualName(visualName);
|
|
43
|
+
return {
|
|
44
|
+
name: name,
|
|
45
|
+
displayName: visualName,
|
|
46
|
+
guid: name + VisualGenerator.generateVisualGuid(),
|
|
47
|
+
visualClassName: 'Visual',
|
|
48
|
+
apiVersion: apiVersion
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
*
|
|
53
|
+
*/
|
|
54
|
+
function generateVisualName(displayName) {
|
|
55
|
+
return displayName.replace(/(?:^\w|[A-Z]|\b\w|_|\s+)/g, (match, index) => {
|
|
56
|
+
if (/\s|_+/.test(match)) {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
return index === 0 ? match.toLowerCase() : match.toUpperCase();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Creates a default pbiviz.json config file
|
|
64
|
+
*
|
|
65
|
+
* @param {string} visualPath - path to the visual
|
|
66
|
+
* @param {Object} options - visual information for populating the pbiviz.json template
|
|
67
|
+
* @param {string} templateName - external js files
|
|
68
|
+
*/
|
|
69
|
+
function createPbiVizJson(visualPath, options, templateName) {
|
|
70
|
+
// read the global template data
|
|
71
|
+
// and generate the actual file content
|
|
72
|
+
let data = template(options);
|
|
73
|
+
// write out the target file content
|
|
74
|
+
const targetPath = path.join(visualPath, 'pbiviz.json');
|
|
75
|
+
fs.writeFileSync(targetPath, data);
|
|
76
|
+
let templatePath = path.join(VISUAL_TEMPLATES_PATH, templateName);
|
|
77
|
+
templatePath = path.join(templatePath, 'pbiviz.json');
|
|
78
|
+
if (templateName && fileExists(templatePath)) {
|
|
79
|
+
//read the target file content
|
|
80
|
+
data = fs.readJsonSync(targetPath);
|
|
81
|
+
//override externalJS settings with those of the local template file
|
|
82
|
+
const templateData = fs.readJsonSync(templatePath);
|
|
83
|
+
for (const objKey of Object.keys(templateData)) {
|
|
84
|
+
data[objKey] = templateData[objKey];
|
|
85
|
+
}
|
|
86
|
+
// write out the target file content
|
|
87
|
+
fs.writeJsonSync(targetPath, data);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Checks if the specified file exists
|
|
92
|
+
*
|
|
93
|
+
* @param {string} file - path to the file
|
|
94
|
+
*/
|
|
95
|
+
function fileExists(file) {
|
|
96
|
+
try {
|
|
97
|
+
fs.accessSync(file);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Copies the visual template directory
|
|
106
|
+
*
|
|
107
|
+
* @param {string} targetPath - file path to root of visual package
|
|
108
|
+
* @param {string} templateName - template to use for generating the visual
|
|
109
|
+
*/
|
|
110
|
+
function copyVisualTemplate(targetPath, templateName) {
|
|
111
|
+
fs.copySync(path.join(VISUAL_TEMPLATES_PATH, '_global'), targetPath);
|
|
112
|
+
fs.copySync(path.join(VISUAL_TEMPLATES_PATH, templateName), targetPath);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Checks if the specified template is valid
|
|
116
|
+
*
|
|
117
|
+
* @param {string} templateName - template to use for generating the visual
|
|
118
|
+
*/
|
|
119
|
+
function validTemplate(templateName) {
|
|
120
|
+
try {
|
|
121
|
+
fs.accessSync(path.join(VISUAL_TEMPLATES_PATH, templateName));
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
const defaultOptions = {
|
|
129
|
+
force: false,
|
|
130
|
+
template: 'default',
|
|
131
|
+
apiVersion: API_VERSION,
|
|
132
|
+
externalJS: []
|
|
133
|
+
};
|
|
134
|
+
export default class VisualGenerator {
|
|
135
|
+
/**
|
|
136
|
+
* Generates a new visual
|
|
137
|
+
*
|
|
138
|
+
* @param {string} targetPath - file path for creation of the new visual package
|
|
139
|
+
* @param {string} visualName - name of the new visual package
|
|
140
|
+
* @param {object} options - specify options for the visual generator
|
|
141
|
+
* @returns {Promise<string>} - promise resolves with the path to the newly created package
|
|
142
|
+
*/
|
|
143
|
+
static generateVisual(targetPath, visualName, options) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const buildOptions = lodashDefaults(options, defaultOptions);
|
|
146
|
+
if (!buildOptions.apiVersion || compareVersions(buildOptions.apiVersion, minAPIversion) === -1) {
|
|
147
|
+
return reject(new Error(`Can not generate a visual with an API below than ${minAPIversion}, current API is '${buildOptions.apiVersion}'.`));
|
|
148
|
+
}
|
|
149
|
+
const visualOptions = generateVisualOptions(visualName, buildOptions.apiVersion);
|
|
150
|
+
const validationResult = VisualGenerator.checkVisualName(visualOptions.name);
|
|
151
|
+
if (!visualOptions || !visualOptions.name || validationResult) {
|
|
152
|
+
return reject(new Error(validationResult || "Invalid visual name"));
|
|
153
|
+
}
|
|
154
|
+
if (!validTemplate(buildOptions.template)) {
|
|
155
|
+
return reject(new Error(`Invalid template "${buildOptions.template}"`));
|
|
156
|
+
}
|
|
157
|
+
const visualPath = path.join(targetPath, visualOptions.name);
|
|
158
|
+
fs.access(visualPath, err => {
|
|
159
|
+
if (!err && !buildOptions.force) {
|
|
160
|
+
return reject(new Error('This visual already exists. Use force to overwrite.'));
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
if (!err && buildOptions.force) {
|
|
164
|
+
fs.removeSync(visualPath);
|
|
165
|
+
}
|
|
166
|
+
copyVisualTemplate(visualPath, buildOptions.template);
|
|
167
|
+
createPbiVizJson(visualPath, visualOptions, options.template);
|
|
168
|
+
resolve(visualPath);
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
reject(e);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Generates a random GUID for your visual
|
|
178
|
+
*/
|
|
179
|
+
static generateVisualGuid() {
|
|
180
|
+
return crypto.randomUUID().replace(/-/g, '').toUpperCase();
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Check visual name
|
|
184
|
+
* Using https://github.com/mathiasbynens/mothereff.in/tree/master/js-properties
|
|
185
|
+
*
|
|
186
|
+
* @static
|
|
187
|
+
* @param {string} name Visual name
|
|
188
|
+
* @returns {string} error message
|
|
189
|
+
*
|
|
190
|
+
* @memberof VisualGenerator
|
|
191
|
+
*/
|
|
192
|
+
static checkVisualName(name) {
|
|
193
|
+
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)$/;
|
|
194
|
+
const regexNumber = /^(?![+-])([0-9\+\-\.]+)/;
|
|
195
|
+
const regexZeroWidth = /\u200c|\u200d/;
|
|
196
|
+
const regexpWrongSymbols = /^[a-zA-Z0-9]+$/;
|
|
197
|
+
const valueAsUnescapedString = name.replace(/\\u([a-fA-F0-9]{4})|\\u\{([0-9a-fA-F]{1,})\}/g, ($0, $1, $2) => {
|
|
198
|
+
const codePoint = parseInt($2 || $1, 16);
|
|
199
|
+
// If it’s a surrogate…
|
|
200
|
+
if (codePoint >= 0xD800 && codePoint <= 0xDFFF) {
|
|
201
|
+
// Return a character that is never valid in an identifier.
|
|
202
|
+
// This prevents the surrogate from pairing with another.
|
|
203
|
+
return '\0';
|
|
204
|
+
}
|
|
205
|
+
return String.fromCodePoint(codePoint);
|
|
206
|
+
});
|
|
207
|
+
if (regexNumber.test(name)) {
|
|
208
|
+
return `The visual name can't begin with a number digit`;
|
|
209
|
+
}
|
|
210
|
+
else if (!regexpWrongSymbols.test(name)) {
|
|
211
|
+
return `The visual name can contain only letters and numbers`;
|
|
212
|
+
}
|
|
213
|
+
else if (regexES3ReservedWord.test(valueAsUnescapedString)) {
|
|
214
|
+
return `The visual name cannot be equal to a reserved JavaScript keyword.
|
|
215
|
+
More information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords`;
|
|
216
|
+
}
|
|
217
|
+
else if (regexZeroWidth.test(valueAsUnescapedString)) {
|
|
218
|
+
return `The visual name can't be empty`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
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
|
+
"use strict";
|
|
27
|
+
import webpack from "webpack";
|
|
28
|
+
import WebpackDevServer from "webpack-dev-server";
|
|
29
|
+
import childProcess from 'child_process';
|
|
30
|
+
import fs from 'fs-extra';
|
|
31
|
+
import path from 'path';
|
|
32
|
+
import ConsoleWriter from './ConsoleWriter.js';
|
|
33
|
+
import VisualGenerator from './VisualGenerator.js';
|
|
34
|
+
import { readJsonFromRoot, readJsonFromVisual } from './utils.js';
|
|
35
|
+
import WebpackWrap from './WebPackWrap.js';
|
|
36
|
+
import Package from './Package.js';
|
|
37
|
+
import { Visual } from "./Visual.js";
|
|
38
|
+
import { FeatureManager, Status } from "./FeatureManager.js";
|
|
39
|
+
import { Severity, Stage } from "./features/FeatureTypes.js";
|
|
40
|
+
import TemplateFetcher from "./TemplateFetcher.js";
|
|
41
|
+
import { LintValidator } from "./LintValidator.js";
|
|
42
|
+
const globalConfig = await readJsonFromRoot('config.json');
|
|
43
|
+
const PBIVIZ_FILE = 'pbiviz.json';
|
|
44
|
+
/**
|
|
45
|
+
* Represents an instance of a visual package based on file path
|
|
46
|
+
*/
|
|
47
|
+
export default class VisualManager {
|
|
48
|
+
basePath;
|
|
49
|
+
pbivizConfig;
|
|
50
|
+
capabilities;
|
|
51
|
+
webpackConfig;
|
|
52
|
+
visual;
|
|
53
|
+
package;
|
|
54
|
+
featureManager;
|
|
55
|
+
compiler;
|
|
56
|
+
webpackDevServer;
|
|
57
|
+
constructor(rootPath) {
|
|
58
|
+
this.basePath = rootPath;
|
|
59
|
+
}
|
|
60
|
+
async prepareVisual(pbivizFile = PBIVIZ_FILE) {
|
|
61
|
+
this.pbivizConfig = await readJsonFromVisual(pbivizFile, this.basePath);
|
|
62
|
+
if (this.pbivizConfig) {
|
|
63
|
+
await this.createVisualInstance();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
ConsoleWriter.error(pbivizFile + ' not found. You must be in the root of a visual project to run this command.');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
async runLintValidation(options) {
|
|
72
|
+
try {
|
|
73
|
+
const linter = new LintValidator(options);
|
|
74
|
+
await linter.runLintValidation();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
ConsoleWriter.error("Can't run lint validation.");
|
|
78
|
+
if (options.verbose) {
|
|
79
|
+
ConsoleWriter.error(error.message);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async createVisualInstance() {
|
|
84
|
+
this.capabilities = await readJsonFromVisual("capabilities.json", this.basePath);
|
|
85
|
+
this.visual = new Visual(this.capabilities, this.pbivizConfig);
|
|
86
|
+
}
|
|
87
|
+
async initializeWebpack(webpackOptions) {
|
|
88
|
+
const webpackWrap = new WebpackWrap();
|
|
89
|
+
this.webpackConfig = await webpackWrap.generateWebpackConfig(this, webpackOptions);
|
|
90
|
+
this.compiler = webpack(this.webpackConfig);
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
generatePackage(verbose = false) {
|
|
94
|
+
const callback = (err, stats) => {
|
|
95
|
+
this.parseCompilationResults(err, stats);
|
|
96
|
+
this.createPackageInstance();
|
|
97
|
+
const logs = this.validatePackage();
|
|
98
|
+
this.outputResults(logs, verbose);
|
|
99
|
+
};
|
|
100
|
+
this.compiler.run(callback);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Starts webpack server
|
|
104
|
+
*/
|
|
105
|
+
startWebpackServer(generateDropFiles = false) {
|
|
106
|
+
ConsoleWriter.blank();
|
|
107
|
+
ConsoleWriter.info('Starting server...');
|
|
108
|
+
try {
|
|
109
|
+
if (generateDropFiles) {
|
|
110
|
+
this.prepareDropFiles();
|
|
111
|
+
}
|
|
112
|
+
this.webpackDevServer = new WebpackDevServer({
|
|
113
|
+
...this.webpackConfig.devServer,
|
|
114
|
+
client: false,
|
|
115
|
+
hot: false,
|
|
116
|
+
devMiddleware: {
|
|
117
|
+
writeToDisk: true
|
|
118
|
+
}
|
|
119
|
+
}, this.compiler);
|
|
120
|
+
(async () => {
|
|
121
|
+
await this.webpackDevServer.start();
|
|
122
|
+
ConsoleWriter.info(`Server listening on port ${this.webpackConfig.devServer.port}`);
|
|
123
|
+
})();
|
|
124
|
+
process.on('SIGINT', this.stopServer);
|
|
125
|
+
process.on('SIGTERM', this.stopServer);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
ConsoleWriter.error(e.message);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Validates the visual code
|
|
134
|
+
*/
|
|
135
|
+
validateVisual(verbose = false) {
|
|
136
|
+
this.featureManager = new FeatureManager();
|
|
137
|
+
const { status, logs } = this.featureManager.validate(Stage.PreBuild, this.visual);
|
|
138
|
+
this.outputResults(logs, verbose);
|
|
139
|
+
if (status === Status.Error) {
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Validates the visual package
|
|
146
|
+
*/
|
|
147
|
+
validatePackage() {
|
|
148
|
+
const featureManager = new FeatureManager();
|
|
149
|
+
const { logs } = featureManager.validate(Stage.PostBuild, this.package);
|
|
150
|
+
return logs;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Outputs the results of the validation
|
|
154
|
+
*/
|
|
155
|
+
outputResults({ errors, deprecation, warnings, info }, verbose) {
|
|
156
|
+
const headerMessage = {
|
|
157
|
+
error: `Visual doesn't support some features required for all custom visuals:`,
|
|
158
|
+
deprecation: `Some features are going to be required soon, please update the visual:`,
|
|
159
|
+
warning: `Visual doesn't support some features recommended for all custom visuals:`,
|
|
160
|
+
verboseInfo: `Visual can be improved by adding some features:`,
|
|
161
|
+
shortInfo: `Visual can be improved by adding ${info.length} more optional features.`
|
|
162
|
+
};
|
|
163
|
+
this.outputLogsWithHeadMessage(headerMessage.error, errors, Severity.Error);
|
|
164
|
+
this.outputLogsWithHeadMessage(headerMessage.deprecation, deprecation, Severity.Deprecation);
|
|
165
|
+
this.outputLogsWithHeadMessage(headerMessage.warning, warnings, Severity.Warning);
|
|
166
|
+
const verboseSuggestion = 'Run `pbiviz package` with --verbose flag to see more details.';
|
|
167
|
+
const headerInfoMessage = headerMessage[verbose ? "verboseInfo" : "shortInfo"];
|
|
168
|
+
const infoLogs = (!info.length || verbose) ? info : [verboseSuggestion];
|
|
169
|
+
this.outputLogsWithHeadMessage(headerInfoMessage, infoLogs, Severity.Info);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Displays visual info
|
|
173
|
+
*/
|
|
174
|
+
displayInfo() {
|
|
175
|
+
if (this.pbivizConfig) {
|
|
176
|
+
ConsoleWriter.infoTable(this.pbivizConfig);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
ConsoleWriter.error('Unable to load visual info. Please ensure the package is valid.');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Creates a new visual
|
|
184
|
+
*/
|
|
185
|
+
static async createVisual(rootPath, visualName, generateOptions) {
|
|
186
|
+
ConsoleWriter.info('Creating new visual');
|
|
187
|
+
if (generateOptions.force) {
|
|
188
|
+
ConsoleWriter.warning('Running with force flag. Existing files will be overwritten');
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const config = await readJsonFromRoot('config.json');
|
|
192
|
+
if (config.visualTemplates[generateOptions.template]) {
|
|
193
|
+
new TemplateFetcher(generateOptions.template, visualName, undefined)
|
|
194
|
+
.fetch();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const newVisualPath = await VisualGenerator.generateVisual(rootPath, visualName, generateOptions);
|
|
198
|
+
await VisualManager.installPackages(newVisualPath).then(() => ConsoleWriter.done('Visual creation complete'));
|
|
199
|
+
return new VisualManager(newVisualPath);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
ConsoleWriter.error(['Unable to create visual.\n', error]);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Install npm dependencies for visual
|
|
208
|
+
*/
|
|
209
|
+
static installPackages(visualPath) {
|
|
210
|
+
return new Promise(function (resolve, reject) {
|
|
211
|
+
ConsoleWriter.info('Installing packages...');
|
|
212
|
+
childProcess.exec(`npm install`, { cwd: visualPath }, (err) => {
|
|
213
|
+
if (err) {
|
|
214
|
+
reject(new Error('Package install failed.'));
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
ConsoleWriter.info('Installed packages.');
|
|
218
|
+
resolve();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
doesPBIVIZExists(pbivizFile) {
|
|
224
|
+
return fs.existsSync(pbivizFile);
|
|
225
|
+
}
|
|
226
|
+
prepareDropFiles() {
|
|
227
|
+
this.webpackConfig.devServer.setupMiddlewares = (middlewares, devServer) => {
|
|
228
|
+
const { headers, publicPath, static: { directory } } = this.webpackConfig.devServer;
|
|
229
|
+
const assets = ['visual.js', 'visual.css', 'pbiviz.json'];
|
|
230
|
+
const setHeaders = (res) => {
|
|
231
|
+
Object.getOwnPropertyNames(headers)
|
|
232
|
+
.forEach(property => res.header(property, headers[property]));
|
|
233
|
+
};
|
|
234
|
+
const readFile = (file, res, name) => {
|
|
235
|
+
const assetMiddleware = (req, middlewareRes, next) => {
|
|
236
|
+
fs.readFile(file)
|
|
237
|
+
.then(content => {
|
|
238
|
+
middlewareRes.write(content);
|
|
239
|
+
ConsoleWriter.info(`Serving ${name}`);
|
|
240
|
+
middlewareRes.end();
|
|
241
|
+
})
|
|
242
|
+
.catch(err => {
|
|
243
|
+
ConsoleWriter.error(`Error serving ${name}: ${err.message}`);
|
|
244
|
+
next();
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
middlewares.unshift({
|
|
248
|
+
name,
|
|
249
|
+
path: `${publicPath}/${name}`,
|
|
250
|
+
middleware: assetMiddleware
|
|
251
|
+
});
|
|
252
|
+
res.end();
|
|
253
|
+
};
|
|
254
|
+
assets.forEach(asset => {
|
|
255
|
+
devServer.app.get(`${publicPath}/${asset}`, function (req, res) {
|
|
256
|
+
setHeaders(res);
|
|
257
|
+
readFile(path.join(directory, asset), res, asset);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
return middlewares;
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async stopServer() {
|
|
264
|
+
ConsoleWriter.blank();
|
|
265
|
+
ConsoleWriter.info("Stopping server...");
|
|
266
|
+
if (this.webpackDevServer) {
|
|
267
|
+
await this.webpackDevServer.stop();
|
|
268
|
+
this.webpackDevServer = null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
createPackageInstance() {
|
|
272
|
+
const pathToJSContent = path.join((this.pbivizConfig.build ?? globalConfig.build).dropFolder, "visual.js");
|
|
273
|
+
const sourceCode = fs.readFileSync(pathToJSContent, "utf8");
|
|
274
|
+
this.package = new Package(sourceCode, this.capabilities, this.visual.visualFeatureType);
|
|
275
|
+
}
|
|
276
|
+
parseCompilationResults(err, stats) {
|
|
277
|
+
ConsoleWriter.blank();
|
|
278
|
+
if (err) {
|
|
279
|
+
ConsoleWriter.error(`Package wasn't created. ${JSON.stringify(err)}`);
|
|
280
|
+
}
|
|
281
|
+
if (stats?.compilation.errors.length) {
|
|
282
|
+
stats.compilation.errors.forEach(error => ConsoleWriter.error(error.message));
|
|
283
|
+
ConsoleWriter.error(`Package wasn't created. ${stats.compilation.errors.length} errors found.`);
|
|
284
|
+
}
|
|
285
|
+
if (!err && !stats?.compilation.errors.length) {
|
|
286
|
+
ConsoleWriter.done('Build completed successfully');
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
outputLogsWithHeadMessage(headMessage, logs, severity) {
|
|
293
|
+
if (!logs.length) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
let outputLog;
|
|
297
|
+
switch (severity) {
|
|
298
|
+
case Severity.Deprecation:
|
|
299
|
+
case Severity.Error:
|
|
300
|
+
outputLog = ConsoleWriter.error;
|
|
301
|
+
break;
|
|
302
|
+
case Severity.Warning:
|
|
303
|
+
outputLog = ConsoleWriter.warning;
|
|
304
|
+
break;
|
|
305
|
+
default:
|
|
306
|
+
outputLog = ConsoleWriter.info;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
if (headMessage) {
|
|
310
|
+
outputLog(headMessage);
|
|
311
|
+
ConsoleWriter.blank();
|
|
312
|
+
}
|
|
313
|
+
logs.forEach(error => outputLog(error));
|
|
314
|
+
ConsoleWriter.blank();
|
|
315
|
+
}
|
|
316
|
+
}
|