powerbi-visuals-tools 7.0.2 → 7.1.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.
@@ -0,0 +1,380 @@
1
+ /*
2
+ * Power BI Visual CLI - MCP Server - Certification Preparation Tool
3
+ *
4
+ * Copyright (c) Microsoft Corporation
5
+ * All rights reserved.
6
+ * MIT License
7
+ */
8
+ "use strict";
9
+ import fs from 'fs-extra';
10
+ import path from 'path';
11
+ import { getSourceFiles, existsIgnoreCase } from '../../utils.js';
12
+ const REQUIRED_FILES = [
13
+ { file: 'pbiviz.json', description: 'Visual configuration file' },
14
+ { file: 'capabilities.json', description: 'Visual capabilities definition' },
15
+ { file: 'package.json', description: 'Node.js package configuration' },
16
+ { file: 'tsconfig.json', description: 'TypeScript configuration' },
17
+ ];
18
+ const RECOMMENDED_FILES = [
19
+ { file: 'README.md', description: 'Documentation' },
20
+ { file: 'CHANGELOG.md', description: 'Version history' },
21
+ { file: 'LICENSE', description: 'License file' },
22
+ ];
23
+ async function checkRequiredFiles(rootPath) {
24
+ const checks = [];
25
+ for (const { file, description } of REQUIRED_FILES) {
26
+ const exists = existsIgnoreCase(path.join(rootPath, file));
27
+ checks.push({
28
+ name: `Required: ${file}`,
29
+ status: exists ? 'pass' : 'fail',
30
+ message: exists ? `${description} found` : `Missing ${description}`,
31
+ recommendation: exists ? undefined : `Create ${file} in project root`
32
+ });
33
+ }
34
+ for (const { file, description } of RECOMMENDED_FILES) {
35
+ const exists = existsIgnoreCase(path.join(rootPath, file));
36
+ checks.push({
37
+ name: `Recommended: ${file}`,
38
+ status: exists ? 'pass' : 'warning',
39
+ message: exists ? `${description} found` : `Missing ${description}`,
40
+ recommendation: exists ? undefined : `Consider adding ${file}`
41
+ });
42
+ }
43
+ return checks;
44
+ }
45
+ async function checkPbivizConfig(rootPath) {
46
+ const checks = [];
47
+ const pbivizPath = path.join(rootPath, 'pbiviz.json');
48
+ if (!fs.existsSync(pbivizPath)) {
49
+ return [{
50
+ name: 'pbiviz.json validation',
51
+ status: 'fail',
52
+ message: 'Cannot validate - pbiviz.json not found'
53
+ }];
54
+ }
55
+ const config = await fs.readJson(pbivizPath);
56
+ // Check visual name
57
+ if (config.visual?.name) {
58
+ checks.push({
59
+ name: 'Visual Name',
60
+ status: 'pass',
61
+ message: `Visual name: ${config.visual.name}`
62
+ });
63
+ }
64
+ else {
65
+ checks.push({
66
+ name: 'Visual Name',
67
+ status: 'fail',
68
+ message: 'Visual name is missing',
69
+ recommendation: 'Add "visual.name" to pbiviz.json'
70
+ });
71
+ }
72
+ // Check GUID
73
+ if (config.visual?.guid && /^[a-zA-Z0-9_]+$/.test(config.visual.guid)) {
74
+ checks.push({
75
+ name: 'Visual GUID',
76
+ status: 'pass',
77
+ message: `GUID: ${config.visual.guid}`
78
+ });
79
+ }
80
+ else {
81
+ checks.push({
82
+ name: 'Visual GUID',
83
+ status: 'fail',
84
+ message: 'Invalid or missing visual GUID',
85
+ recommendation: 'Ensure visual.guid contains only alphanumeric characters and underscores'
86
+ });
87
+ }
88
+ // Check version format
89
+ const version = config.visual?.version;
90
+ if (version && /^\d+\.\d+\.\d+(\.\d+)?$/.test(version)) {
91
+ checks.push({
92
+ name: 'Visual Version',
93
+ status: 'pass',
94
+ message: `Version: ${version}`
95
+ });
96
+ }
97
+ else {
98
+ checks.push({
99
+ name: 'Visual Version',
100
+ status: 'fail',
101
+ message: 'Invalid version format',
102
+ recommendation: 'Use semantic versioning (e.g., 1.0.0 or 1.0.0.0)'
103
+ });
104
+ }
105
+ // Check API version
106
+ const apiVersion = config.apiVersion;
107
+ if (apiVersion) {
108
+ const majorVersion = parseInt(apiVersion.split('.')[0], 10);
109
+ if (majorVersion >= 3) {
110
+ checks.push({
111
+ name: 'API Version',
112
+ status: 'pass',
113
+ message: `API version: ${apiVersion}`
114
+ });
115
+ }
116
+ else {
117
+ checks.push({
118
+ name: 'API Version',
119
+ status: 'warning',
120
+ message: `API version ${apiVersion} is outdated`,
121
+ recommendation: 'Update to API version 5.x or higher'
122
+ });
123
+ }
124
+ }
125
+ else {
126
+ checks.push({
127
+ name: 'API Version',
128
+ status: 'fail',
129
+ message: 'API version not specified',
130
+ recommendation: 'Add "apiVersion" to pbiviz.json'
131
+ });
132
+ }
133
+ // Check author info
134
+ if (config.author?.name && config.author?.email) {
135
+ checks.push({
136
+ name: 'Author Information',
137
+ status: 'pass',
138
+ message: `Author: ${config.author.name} <${config.author.email}>`
139
+ });
140
+ }
141
+ else {
142
+ checks.push({
143
+ name: 'Author Information',
144
+ status: 'warning',
145
+ message: 'Incomplete author information',
146
+ recommendation: 'Add author.name and author.email to pbiviz.json'
147
+ });
148
+ }
149
+ // Check support URL
150
+ if (config.visual?.supportUrl) {
151
+ checks.push({
152
+ name: 'Support URL',
153
+ status: 'pass',
154
+ message: `Support URL: ${config.visual.supportUrl}`
155
+ });
156
+ }
157
+ else {
158
+ checks.push({
159
+ name: 'Support URL',
160
+ status: 'warning',
161
+ message: 'Support URL not provided',
162
+ recommendation: 'Add visual.supportUrl for user support'
163
+ });
164
+ }
165
+ return checks;
166
+ }
167
+ async function checkCapabilities(rootPath) {
168
+ const checks = [];
169
+ const capabilitiesPath = path.join(rootPath, 'capabilities.json');
170
+ if (!fs.existsSync(capabilitiesPath)) {
171
+ return [{
172
+ name: 'capabilities.json validation',
173
+ status: 'fail',
174
+ message: 'Cannot validate - capabilities.json not found'
175
+ }];
176
+ }
177
+ const capabilities = await fs.readJson(capabilitiesPath);
178
+ // Check data roles
179
+ if (capabilities.dataRoles && Array.isArray(capabilities.dataRoles) && capabilities.dataRoles.length > 0) {
180
+ checks.push({
181
+ name: 'Data Roles',
182
+ status: 'pass',
183
+ message: `${capabilities.dataRoles.length} data role(s) defined`
184
+ });
185
+ }
186
+ else {
187
+ checks.push({
188
+ name: 'Data Roles',
189
+ status: 'warning',
190
+ message: 'No data roles defined',
191
+ recommendation: 'Define dataRoles in capabilities.json for data binding'
192
+ });
193
+ }
194
+ // Check data view mappings
195
+ if (capabilities.dataViewMappings && Array.isArray(capabilities.dataViewMappings)) {
196
+ checks.push({
197
+ name: 'Data View Mappings',
198
+ status: 'pass',
199
+ message: `${capabilities.dataViewMappings.length} mapping(s) defined`
200
+ });
201
+ }
202
+ else {
203
+ checks.push({
204
+ name: 'Data View Mappings',
205
+ status: 'fail',
206
+ message: 'No dataViewMappings defined',
207
+ recommendation: 'Define dataViewMappings in capabilities.json'
208
+ });
209
+ }
210
+ // Check privileges (for certification, should be minimal)
211
+ if (capabilities.privileges) {
212
+ const privs = capabilities.privileges;
213
+ if (privs.some(p => p.name === 'WebAccess' && p.essential)) {
214
+ checks.push({
215
+ name: 'Web Access Privilege',
216
+ status: 'warning',
217
+ message: 'WebAccess privilege is enabled',
218
+ recommendation: 'External web access may prevent certification. Remove if not needed.'
219
+ });
220
+ }
221
+ }
222
+ // Check supportsHighlight
223
+ if (capabilities.supportsHighlight) {
224
+ checks.push({
225
+ name: 'Highlight Support',
226
+ status: 'pass',
227
+ message: 'Visual supports data highlighting'
228
+ });
229
+ }
230
+ // Check supportsKeyboardFocus (accessibility)
231
+ if (capabilities.supportsKeyboardFocus) {
232
+ checks.push({
233
+ name: 'Keyboard Focus Support',
234
+ status: 'pass',
235
+ message: 'Visual supports keyboard navigation (accessibility)'
236
+ });
237
+ }
238
+ else {
239
+ checks.push({
240
+ name: 'Keyboard Focus Support',
241
+ status: 'warning',
242
+ message: 'Keyboard navigation not enabled',
243
+ recommendation: 'Add "supportsKeyboardFocus": true for accessibility'
244
+ });
245
+ }
246
+ return checks;
247
+ }
248
+ async function checkRenderingEvents(rootPath) {
249
+ const checks = [];
250
+ const srcPath = path.join(rootPath, 'src');
251
+ if (!fs.existsSync(srcPath)) {
252
+ checks.push({
253
+ name: 'Rendering Events',
254
+ status: 'fail',
255
+ message: 'Cannot check — src/ folder not found'
256
+ });
257
+ return checks;
258
+ }
259
+ const sourceFiles = await getSourceFiles(srcPath);
260
+ let allCode = '';
261
+ for (const file of sourceFiles) {
262
+ allCode += await fs.readFile(file, 'utf-8') + '\n';
263
+ }
264
+ const hasStarted = /renderingStarted/.test(allCode);
265
+ const hasFinished = /renderingFinished/.test(allCode);
266
+ const hasFailed = /renderingFailed/.test(allCode);
267
+ if (hasStarted && hasFinished && hasFailed) {
268
+ checks.push({
269
+ name: 'Rendering Events',
270
+ status: 'pass',
271
+ message: 'All 3 rendering events implemented (renderingStarted, renderingFinished, renderingFailed)'
272
+ });
273
+ }
274
+ else {
275
+ const missing = [];
276
+ if (!hasStarted)
277
+ missing.push('renderingStarted');
278
+ if (!hasFinished)
279
+ missing.push('renderingFinished');
280
+ if (!hasFailed)
281
+ missing.push('renderingFailed');
282
+ checks.push({
283
+ name: 'Rendering Events',
284
+ status: 'fail',
285
+ message: `Missing rendering events: ${missing.join(', ')}`,
286
+ recommendation: 'Rendering events are required for certification. Call host.eventService.renderingStarted(options) at the beginning of update(), renderingFinished(options) on success, and renderingFailed(options, error) on error.'
287
+ });
288
+ }
289
+ return checks;
290
+ }
291
+ async function checkAssets(rootPath) {
292
+ const checks = [];
293
+ const assetsPath = path.join(rootPath, 'assets');
294
+ if (!fs.existsSync(assetsPath)) {
295
+ checks.push({
296
+ name: 'Assets Folder',
297
+ status: 'warning',
298
+ message: 'Assets folder not found',
299
+ recommendation: 'Create assets/ folder with icon.png (20x20)'
300
+ });
301
+ return checks;
302
+ }
303
+ const iconPath = path.join(assetsPath, 'icon.png');
304
+ if (fs.existsSync(iconPath)) {
305
+ checks.push({
306
+ name: 'Visual Icon',
307
+ status: 'pass',
308
+ message: 'icon.png found in assets/'
309
+ });
310
+ }
311
+ else {
312
+ checks.push({
313
+ name: 'Visual Icon',
314
+ status: 'warning',
315
+ message: 'icon.png not found',
316
+ recommendation: 'Add icon.png (20x20 px) to assets/ folder'
317
+ });
318
+ }
319
+ return checks;
320
+ }
321
+ function formatResults(checks) {
322
+ const passed = checks.filter(c => c.status === 'pass').length;
323
+ const failed = checks.filter(c => c.status === 'fail').length;
324
+ const warnings = checks.filter(c => c.status === 'warning').length;
325
+ let output = `# 📋 Certification Readiness Report\n\n`;
326
+ output += `| Status | Count |\n|--------|-------|\n`;
327
+ output += `| ✅ Passed | ${passed} |\n`;
328
+ output += `| ❌ Failed | ${failed} |\n`;
329
+ output += `| ⚠️ Warnings | ${warnings} |\n\n`;
330
+ if (failed === 0 && warnings === 0) {
331
+ output += `## 🎉 Excellent! Your visual is ready for certification!\n\n`;
332
+ }
333
+ else if (failed === 0) {
334
+ output += `## 👍 Good! Fix warnings for a better certification experience.\n\n`;
335
+ }
336
+ else {
337
+ output += `## 🔧 Action Required: Fix failed checks before certification.\n\n`;
338
+ }
339
+ // Group by status
340
+ if (failed > 0) {
341
+ output += `## ❌ Failed Checks\n`;
342
+ checks.filter(c => c.status === 'fail').forEach(c => {
343
+ output += `- **${c.name}**: ${c.message}\n`;
344
+ if (c.recommendation)
345
+ output += ` 💡 ${c.recommendation}\n`;
346
+ });
347
+ output += '\n';
348
+ }
349
+ if (warnings > 0) {
350
+ output += `## ⚠️ Warnings\n`;
351
+ checks.filter(c => c.status === 'warning').forEach(c => {
352
+ output += `- **${c.name}**: ${c.message}\n`;
353
+ if (c.recommendation)
354
+ output += ` 💡 ${c.recommendation}\n`;
355
+ });
356
+ output += '\n';
357
+ }
358
+ output += `## ✅ Passed Checks\n`;
359
+ checks.filter(c => c.status === 'pass').forEach(c => {
360
+ output += `- **${c.name}**: ${c.message}\n`;
361
+ });
362
+ output += `\n---\n`;
363
+ output += `For certification: \`pbiviz package --certification-audit\`\n`;
364
+ output += `Learn more: https://learn.microsoft.com/power-bi/developer/visuals/power-bi-custom-visuals-certified\n`;
365
+ return output;
366
+ }
367
+ export async function prepareCertification(rootPath) {
368
+ try {
369
+ const fileChecks = await checkRequiredFiles(rootPath);
370
+ const pbivizChecks = await checkPbivizConfig(rootPath);
371
+ const capabilityChecks = await checkCapabilities(rootPath);
372
+ const renderingChecks = await checkRenderingEvents(rootPath);
373
+ const assetChecks = await checkAssets(rootPath);
374
+ const allChecks = [...fileChecks, ...pbivizChecks, ...capabilityChecks, ...renderingChecks, ...assetChecks];
375
+ return formatResults(allChecks);
376
+ }
377
+ catch (error) {
378
+ return `❌ Error checking certification readiness: ${(error instanceof Error) ? error.message : String(error)}`;
379
+ }
380
+ }
@@ -0,0 +1,133 @@
1
+ /*
2
+ * Power BI Visual CLI - MCP Server - Visual Info Tool
3
+ *
4
+ * Copyright (c) Microsoft Corporation
5
+ * All rights reserved.
6
+ * MIT License
7
+ */
8
+ "use strict";
9
+ import fs from 'fs-extra';
10
+ import path from 'path';
11
+ async function loadJsonSafe(filePath) {
12
+ try {
13
+ if (fs.existsSync(filePath)) {
14
+ return await fs.readJson(filePath);
15
+ }
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ return null;
21
+ }
22
+ export async function getVisualInfo(rootPath) {
23
+ try {
24
+ const pbivizPath = path.join(rootPath, 'pbiviz.json');
25
+ const capabilitiesPath = path.join(rootPath, 'capabilities.json');
26
+ const packageJsonPath = path.join(rootPath, 'package.json');
27
+ const pbiviz = await loadJsonSafe(pbivizPath);
28
+ const capabilities = await loadJsonSafe(capabilitiesPath);
29
+ const packageJson = await loadJsonSafe(packageJsonPath);
30
+ if (!pbiviz) {
31
+ return `❌ **Not a Power BI Visual Project**
32
+
33
+ No pbiviz.json found in: ${rootPath}
34
+
35
+ To create a new visual, run:
36
+ \`\`\`
37
+ pbiviz new <visual-name>
38
+ \`\`\`
39
+ `;
40
+ }
41
+ const info = {
42
+ name: pbiviz.visual?.name || 'Unknown',
43
+ displayName: pbiviz.visual?.displayName || 'Unknown',
44
+ guid: pbiviz.visual?.guid || 'Not set',
45
+ version: pbiviz.visual?.version || '0.0.0',
46
+ apiVersion: pbiviz.apiVersion || 'Not specified',
47
+ author: {
48
+ name: pbiviz.author?.name || 'Not specified',
49
+ email: pbiviz.author?.email || 'Not specified'
50
+ },
51
+ description: pbiviz.visual?.description,
52
+ supportUrl: pbiviz.visual?.supportUrl,
53
+ capabilities: {
54
+ dataRoles: capabilities?.dataRoles?.map(r => r.name) || [],
55
+ dataViewMappings: capabilities?.dataViewMappings?.map((_, i) => `Mapping ${i + 1}`) || [],
56
+ objects: capabilities?.objects ? Object.keys(capabilities.objects) : [],
57
+ supportsHighlight: capabilities?.supportsHighlight || false,
58
+ supportsKeyboardFocus: capabilities?.supportsKeyboardFocus || false,
59
+ supportsLandingPage: capabilities?.supportsLandingPage || false,
60
+ supportsMultiVisualSelection: capabilities?.supportsMultiVisualSelection || false,
61
+ },
62
+ dependencies: packageJson?.dependencies,
63
+ devDependencies: packageJson?.devDependencies
64
+ };
65
+ return formatVisualInfo(info);
66
+ }
67
+ catch (error) {
68
+ return `❌ Error reading visual info: ${(error instanceof Error) ? error.message : String(error)}`;
69
+ }
70
+ }
71
+ function formatVisualInfo(info) {
72
+ let output = `# 📊 Power BI Visual Information\n\n`;
73
+ // Basic Info
74
+ output += `## 🔷 Visual Details\n\n`;
75
+ output += `| Property | Value |\n|----------|-------|\n`;
76
+ output += `| **Name** | ${info.name} |\n`;
77
+ output += `| **Display Name** | ${info.displayName} |\n`;
78
+ output += `| **GUID** | \`${info.guid}\` |\n`;
79
+ output += `| **Version** | ${info.version} |\n`;
80
+ output += `| **API Version** | ${info.apiVersion} |\n`;
81
+ if (info.description) {
82
+ output += `| **Description** | ${info.description} |\n`;
83
+ }
84
+ if (info.supportUrl) {
85
+ output += `| **Support URL** | ${info.supportUrl} |\n`;
86
+ }
87
+ output += '\n';
88
+ // Author
89
+ output += `## 👤 Author\n\n`;
90
+ output += `- **Name**: ${info.author.name}\n`;
91
+ output += `- **Email**: ${info.author.email}\n\n`;
92
+ // Capabilities
93
+ output += `## ⚙️ Capabilities\n\n`;
94
+ if (info.capabilities.dataRoles.length > 0) {
95
+ output += `### Data Roles\n`;
96
+ info.capabilities.dataRoles.forEach(role => {
97
+ output += `- \`${role}\`\n`;
98
+ });
99
+ output += '\n';
100
+ }
101
+ if (info.capabilities.objects.length > 0) {
102
+ output += `### Format Objects (Settings)\n`;
103
+ info.capabilities.objects.forEach(obj => {
104
+ output += `- \`${obj}\`\n`;
105
+ });
106
+ output += '\n';
107
+ }
108
+ output += `### Supported Features\n`;
109
+ output += `| Feature | Enabled |\n|---------|--------|\n`;
110
+ output += `| Highlight | ${info.capabilities.supportsHighlight ? '✅' : '❌'} |\n`;
111
+ output += `| Keyboard Focus | ${info.capabilities.supportsKeyboardFocus ? '✅' : '❌'} |\n`;
112
+ output += `| Landing Page | ${info.capabilities.supportsLandingPage ? '✅' : '❌'} |\n`;
113
+ output += `| Multi-Visual Selection | ${info.capabilities.supportsMultiVisualSelection ? '✅' : '❌'} |\n`;
114
+ output += '\n';
115
+ // Dependencies
116
+ if (info.dependencies && Object.keys(info.dependencies).length > 0) {
117
+ output += `## 📦 Dependencies\n\n`;
118
+ output += `| Package | Version |\n|---------|--------|\n`;
119
+ for (const [pkg, version] of Object.entries(info.dependencies)) {
120
+ output += `| ${pkg} | ${version} |\n`;
121
+ }
122
+ output += '\n';
123
+ }
124
+ // Quick Commands
125
+ output += `## 🚀 Quick Commands\n\n`;
126
+ output += `\`\`\`bash\n`;
127
+ output += `# Start development server\npbiviz start\n\n`;
128
+ output += `# Build package\npbiviz package\n\n`;
129
+ output += `# Run linting\npbiviz lint\n\n`;
130
+ output += `# Check certification readiness\npbiviz package --certification-audit\n`;
131
+ output += `\`\`\`\n`;
132
+ return output;
133
+ }