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,391 @@
1
+ /*
2
+ * Power BI Visual CLI - MCP Server - Best Practices 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
+ async function buildProjectContext(rootPath) {
13
+ const readJsonSafe = async (filePath) => {
14
+ try {
15
+ return await fs.readFile(filePath, 'utf-8');
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ };
21
+ const srcPath = path.join(rootPath, 'src');
22
+ const sourceFiles = await getSourceFiles(srcPath);
23
+ let sourceCode = '';
24
+ for (const file of sourceFiles) {
25
+ sourceCode += await fs.readFile(file, 'utf-8') + '\n';
26
+ }
27
+ return {
28
+ rootPath,
29
+ tsconfigContent: await readJsonSafe(path.join(rootPath, 'tsconfig.json')),
30
+ capabilitiesContent: await readJsonSafe(path.join(rootPath, 'capabilities.json')),
31
+ packageJsonContent: await readJsonSafe(path.join(rootPath, 'package.json')),
32
+ pbivizContent: await readJsonSafe(path.join(rootPath, 'pbiviz.json')),
33
+ sourceCode,
34
+ sourceFiles: sourceFiles.map(f => path.relative(rootPath, f)),
35
+ hasReadme: existsIgnoreCase(path.join(rootPath, 'README.md')),
36
+ hasChangelog: existsIgnoreCase(path.join(rootPath, 'Changelog.md')) || existsIgnoreCase(path.join(rootPath, 'CHANGELOG.md')),
37
+ hasEslint: fs.existsSync(path.join(rootPath, 'eslint.config.mjs')) || fs.existsSync(path.join(rootPath, '.eslintrc.json')) || fs.existsSync(path.join(rootPath, '.eslintrc.js')),
38
+ hasTests: fs.existsSync(path.join(rootPath, 'test')) || fs.existsSync(path.join(rootPath, 'spec')) || fs.existsSync(path.join(rootPath, '__tests__')),
39
+ };
40
+ }
41
+ const PRACTICES = [
42
+ {
43
+ id: 1, section: 'API & Version Management', title: 'Use Latest API Version',
44
+ description: 'Always use the latest stable powerbi-visuals-api (currently v5.x)',
45
+ check: (ctx) => {
46
+ if (!ctx.packageJsonContent)
47
+ return { status: '⚠️', detail: 'package.json not found' };
48
+ const match = ctx.packageJsonContent.match(/"powerbi-visuals-api"\s*:\s*"[~^]?(\d+)/);
49
+ if (!match)
50
+ return { status: '⚠️', detail: 'powerbi-visuals-api not found in dependencies' };
51
+ const major = parseInt(match[1]);
52
+ if (major >= 5)
53
+ return { status: '✅', detail: `Using API v${match[1]}.x` };
54
+ return { status: '⚠️', detail: `Using API v${match[1]}.x — consider upgrading to v5.x` };
55
+ }
56
+ },
57
+ {
58
+ id: 2, section: 'API & Version Management', title: 'TypeScript Strict Mode',
59
+ description: 'Enable strict mode in tsconfig.json for better type safety',
60
+ check: (ctx) => {
61
+ if (!ctx.tsconfigContent)
62
+ return { status: '⚠️', detail: 'tsconfig.json not found' };
63
+ if (/"strict"\s*:\s*true/.test(ctx.tsconfigContent))
64
+ return { status: '✅', detail: 'Strict mode enabled' };
65
+ return { status: '❌', detail: 'Strict mode not enabled — add `"strict": true` to tsconfig.json' };
66
+ }
67
+ },
68
+ {
69
+ id: 3, section: 'Performance', title: 'Minimize Update Loop Work',
70
+ description: 'Cache DOM selections, use data binding efficiently',
71
+ check: (ctx) => {
72
+ const hasUpdate = /update\s*\(/.test(ctx.sourceCode);
73
+ if (!hasUpdate)
74
+ return { status: '⚠️', detail: 'No update() method found' };
75
+ const hasD3Join = /\.join\(/.test(ctx.sourceCode) || /\.enter\(\)/.test(ctx.sourceCode);
76
+ if (hasD3Join)
77
+ return { status: '✅', detail: 'D3 data binding patterns detected' };
78
+ return { status: '⚠️', detail: 'Consider using D3 .join() pattern for efficient data binding' };
79
+ }
80
+ },
81
+ {
82
+ id: 4, section: 'Performance', title: 'FetchMoreData for Large Datasets',
83
+ description: 'Implement pagination for datasets exceeding initial row limit',
84
+ check: (ctx) => {
85
+ if (/fetchMoreData/.test(ctx.sourceCode))
86
+ return { status: '✅', detail: 'fetchMoreData is implemented' };
87
+ return { status: '⚠️', detail: 'Consider implementing fetchMoreData for large datasets' };
88
+ }
89
+ },
90
+ {
91
+ id: 5, section: 'Performance', title: 'Optimize Data Processing',
92
+ description: 'Process data once in update(), store results',
93
+ check: (ctx) => {
94
+ const hasViewModel = /viewModel|ViewModel|dataModel|DataModel/.test(ctx.sourceCode);
95
+ if (hasViewModel)
96
+ return { status: '✅', detail: 'Data model/view model pattern detected' };
97
+ return { status: '⚠️', detail: 'Consider creating a ViewModel to separate data processing from rendering' };
98
+ }
99
+ },
100
+ {
101
+ id: 6, section: 'Security', title: 'No External Network Calls',
102
+ description: 'Avoid fetch/XMLHttpRequest to external URLs',
103
+ check: (ctx) => {
104
+ const hasExternal = /fetch\s*\(\s*['"`]http/.test(ctx.sourceCode) || /XMLHttpRequest/.test(ctx.sourceCode);
105
+ if (hasExternal)
106
+ return { status: '❌', detail: 'External network calls detected — remove for certification' };
107
+ return { status: '✅', detail: 'No external network calls found' };
108
+ }
109
+ },
110
+ {
111
+ id: 7, section: 'Security', title: 'No eval() or Function()',
112
+ description: 'Never use dynamic code execution',
113
+ check: (ctx) => {
114
+ const hasEval = /eval\s*\(/.test(ctx.sourceCode) || /new\s+Function\s*\(/.test(ctx.sourceCode);
115
+ if (hasEval)
116
+ return { status: '❌', detail: 'Dynamic code execution detected — security risk' };
117
+ return { status: '✅', detail: 'No dynamic code execution found' };
118
+ }
119
+ },
120
+ {
121
+ id: 8, section: 'Security', title: 'Sanitize User Input',
122
+ description: 'Escape HTML in tooltips and labels',
123
+ check: (ctx) => {
124
+ const hasInnerHTML = /innerHTML\s*=/.test(ctx.sourceCode);
125
+ if (hasInnerHTML)
126
+ return { status: '⚠️', detail: 'innerHTML usage detected — ensure data is sanitized' };
127
+ return { status: '✅', detail: 'No direct innerHTML assignment found' };
128
+ }
129
+ },
130
+ {
131
+ id: 9, section: 'Accessibility', title: 'Keyboard Navigation',
132
+ description: 'Support Tab navigation and keyboard shortcuts',
133
+ check: (ctx) => {
134
+ const capHasKeys = ctx.capabilitiesContent && /supportsKeyboardFocus/.test(ctx.capabilitiesContent);
135
+ const codeHasKeys = /keydown|keyup|keypress|tabindex|focusable/.test(ctx.sourceCode);
136
+ if (capHasKeys || codeHasKeys)
137
+ return { status: '✅', detail: 'Keyboard navigation support detected' };
138
+ return { status: '⚠️', detail: 'No keyboard navigation detected — required for certification' };
139
+ }
140
+ },
141
+ {
142
+ id: 10, section: 'Accessibility', title: 'High Contrast Mode',
143
+ description: 'Support all high contrast themes',
144
+ check: (ctx) => {
145
+ const hasHC = /highContrast|isHighContrast|allowHighContrast/.test(ctx.sourceCode);
146
+ if (hasHC)
147
+ return { status: '✅', detail: 'High contrast mode support detected' };
148
+ return { status: '⚠️', detail: 'No high contrast support detected — required for certification' };
149
+ }
150
+ },
151
+ {
152
+ id: 11, section: 'Accessibility', title: 'Screen Reader Support',
153
+ description: 'Add proper ARIA labels to interactive elements',
154
+ check: (ctx) => {
155
+ const hasAria = /aria-label|aria-role|role=/.test(ctx.sourceCode);
156
+ if (hasAria)
157
+ return { status: '✅', detail: 'ARIA attributes detected' };
158
+ return { status: '⚠️', detail: 'No ARIA labels found — add for better accessibility' };
159
+ }
160
+ },
161
+ {
162
+ id: 12, section: 'Rendering Events', title: 'Rendering Events (Required)',
163
+ description: 'Report renderingStarted/renderingFinished/renderingFailed events',
164
+ check: (ctx) => {
165
+ const hasStarted = /renderingStarted/.test(ctx.sourceCode);
166
+ const hasFinished = /renderingFinished/.test(ctx.sourceCode);
167
+ const hasFailed = /renderingFailed/.test(ctx.sourceCode);
168
+ if (hasStarted && hasFinished && hasFailed)
169
+ return { status: '✅', detail: 'All 3 rendering events implemented' };
170
+ if (hasStarted || hasFinished)
171
+ return { status: '⚠️', detail: 'Partial rendering events — need renderingStarted, renderingFinished, AND renderingFailed' };
172
+ return { status: '❌', detail: 'No rendering events found — required for certification' };
173
+ }
174
+ },
175
+ {
176
+ id: 13, section: 'Formatting', title: 'Modern Formatting Pane',
177
+ description: 'Use getFormattingModel() for the modern format pane',
178
+ check: (ctx) => {
179
+ if (/getFormattingModel/.test(ctx.sourceCode))
180
+ return { status: '✅', detail: 'Modern formatting pane (getFormattingModel) detected' };
181
+ if (/enumerateObjectInstances/.test(ctx.sourceCode))
182
+ return { status: '⚠️', detail: 'Using legacy format pane — consider migrating to getFormattingModel()' };
183
+ return { status: '⚠️', detail: 'No formatting pane implementation detected' };
184
+ }
185
+ },
186
+ {
187
+ id: 14, section: 'Project Structure', title: 'Modular Code',
188
+ description: 'Split code into logical modules',
189
+ check: (ctx) => {
190
+ if (ctx.sourceFiles.length > 2)
191
+ return { status: '✅', detail: `${ctx.sourceFiles.length} source files — good modular structure` };
192
+ return { status: '⚠️', detail: `Only ${ctx.sourceFiles.length} source file(s) — consider splitting into modules` };
193
+ }
194
+ },
195
+ {
196
+ id: 15, section: 'Project Structure', title: 'Error Handling',
197
+ description: 'Wrap rendering logic in try/catch',
198
+ check: (ctx) => {
199
+ if (/try\s*\{/.test(ctx.sourceCode) && /catch\s*\(/.test(ctx.sourceCode))
200
+ return { status: '✅', detail: 'Error handling (try/catch) detected' };
201
+ return { status: '⚠️', detail: 'No try/catch found — add error handling around rendering logic' };
202
+ }
203
+ },
204
+ {
205
+ id: 16, section: 'Testing', title: 'Tests Present',
206
+ description: 'Unit tests and visual tests',
207
+ check: (ctx) => {
208
+ if (ctx.hasTests)
209
+ return { status: '✅', detail: 'Test directory found' };
210
+ return { status: '⚠️', detail: 'No tests found — add unit tests for data transformation logic' };
211
+ }
212
+ },
213
+ {
214
+ id: 17, section: 'Testing', title: 'ESLint Configured',
215
+ description: 'Use ESLint for code quality',
216
+ check: (ctx) => {
217
+ if (ctx.hasEslint)
218
+ return { status: '✅', detail: 'ESLint configuration found' };
219
+ return { status: '⚠️', detail: 'No ESLint config — add for consistent code quality' };
220
+ }
221
+ },
222
+ {
223
+ id: 18, section: 'Documentation', title: 'README.md',
224
+ description: 'Document visual capabilities and usage',
225
+ check: (ctx) => {
226
+ if (ctx.hasReadme)
227
+ return { status: '✅', detail: 'README.md found' };
228
+ return { status: '❌', detail: 'No README.md — add documentation' };
229
+ }
230
+ },
231
+ {
232
+ id: 19, section: 'Documentation', title: 'Changelog',
233
+ description: 'Track version changes',
234
+ check: (ctx) => {
235
+ if (ctx.hasChangelog)
236
+ return { status: '✅', detail: 'Changelog found' };
237
+ return { status: '⚠️', detail: 'No Changelog — consider adding one' };
238
+ }
239
+ },
240
+ {
241
+ id: 20, section: 'Context Menu', title: 'Context Menu Support',
242
+ description: 'Right-click context menu for drill, filter, and other actions',
243
+ check: (ctx) => {
244
+ if (/contextMenuService/.test(ctx.sourceCode))
245
+ return { status: '✅', detail: 'Context menu implemented' };
246
+ return { status: '⚠️', detail: 'No context menu — add right-click support for better UX' };
247
+ }
248
+ },
249
+ ];
250
+ export async function getBestPractices(rootPath) {
251
+ // If no rootPath, return static list
252
+ if (!rootPath || !fs.existsSync(rootPath)) {
253
+ return getStaticBestPractices();
254
+ }
255
+ // Build project context and check each practice
256
+ const ctx = await buildProjectContext(rootPath);
257
+ let output = `# Power BI Custom Visual Best Practices\n\n`;
258
+ output += `**Project scan results for**: ${path.basename(rootPath)}\n\n`;
259
+ let passed = 0;
260
+ let warnings = 0;
261
+ let failed = 0;
262
+ const sections = new Map();
263
+ for (const practice of PRACTICES) {
264
+ const result = practice.check(ctx);
265
+ if (result.status === '✅')
266
+ passed++;
267
+ else if (result.status === '⚠️')
268
+ warnings++;
269
+ else
270
+ failed++;
271
+ const line = `${result.status} **${practice.id}. ${practice.title}**: ${result.detail}`;
272
+ if (!sections.has(practice.section)) {
273
+ sections.set(practice.section, []);
274
+ }
275
+ sections.get(practice.section).push(line);
276
+ }
277
+ output += `## 📊 Summary: ${passed} passed, ${warnings} warnings, ${failed} issues\n\n`;
278
+ for (const [section, lines] of sections) {
279
+ const sectionEmoji = {
280
+ 'API & Version Management': '🎯',
281
+ 'Performance': '⚡',
282
+ 'Security': '🔒',
283
+ 'Accessibility': '♿',
284
+ 'Rendering Events': '📡',
285
+ 'Formatting': '🎨',
286
+ 'Project Structure': '📦',
287
+ 'Testing': '🧪',
288
+ 'Documentation': '📝',
289
+ 'Context Menu': '🖱️',
290
+ };
291
+ output += `### ${sectionEmoji[section] || '📋'} ${section}\n\n`;
292
+ for (const line of lines) {
293
+ output += `${line}\n\n`;
294
+ }
295
+ }
296
+ output += `---\nFor more details, visit: https://learn.microsoft.com/en-us/power-bi/developer/visuals/\n`;
297
+ return output;
298
+ }
299
+ function getStaticBestPractices() {
300
+ return `# Power BI Custom Visual Best Practices
301
+
302
+ ## 🎯 API & Version Management
303
+
304
+ 1. **Use Latest API Version**: Always use the latest stable powerbi-visuals-api (currently v5.x)
305
+ - Run: \`npm install powerbi-visuals-api@latest\`
306
+ - Update apiVersion in pbiviz.json
307
+
308
+ 2. **TypeScript Strict Mode**: Enable strict mode in tsconfig.json for better type safety
309
+ \`\`\`json
310
+ { "compilerOptions": { "strict": true } }
311
+ \`\`\`
312
+
313
+ ## ⚡ Performance Optimization
314
+
315
+ 3. **Minimize Update Loop Work**: The \`update()\` method is called frequently
316
+ - Cache DOM selections
317
+ - Use data binding efficiently (D3.js .join() pattern)
318
+ - Avoid heavy computations in update()
319
+
320
+ 4. **Use Lazy Loading**: Load resources only when needed
321
+ - Defer non-critical rendering
322
+ - Use requestAnimationFrame for smooth animations
323
+
324
+ 5. **Optimize Data Processing**:
325
+ - Process data once in update(), store results
326
+ - Use Web Workers for heavy calculations
327
+ - Implement pagination with fetchMoreData for large datasets
328
+
329
+ ## 🔒 Security Guidelines
330
+
331
+ 6. **No External Network Calls**: Avoid fetch/XMLHttpRequest to external URLs
332
+ - Use only Power BI host services
333
+ - Required for certification
334
+
335
+ 7. **Sanitize User Input**: Always sanitize data before rendering
336
+ - Escape HTML in tooltips and labels
337
+ - Prevent XSS vulnerabilities
338
+
339
+ 8. **No eval() or Function()**: Never use dynamic code execution
340
+
341
+ ## ♿ Accessibility (Required for Certification)
342
+
343
+ 9. **Keyboard Navigation**: Implement IVisualHost.hostCapabilities
344
+ - Support Tab navigation
345
+ - Provide keyboard shortcuts
346
+
347
+ 10. **High Contrast Mode**: Support all high contrast themes
348
+ - Use host.colorPalette for colors
349
+ - Test with Windows High Contrast
350
+
351
+ 11. **Screen Reader Support**: Add proper ARIA labels
352
+ - role attributes on interactive elements
353
+ - aria-label for data points
354
+
355
+ ## 📦 Project Structure
356
+
357
+ 12. **Modular Code**: Split code into logical modules
358
+ - Separate data transformation, rendering, and formatting
359
+ - Use ES6 modules
360
+
361
+ 13. **Proper Error Handling**: Graceful degradation
362
+ \`\`\`typescript
363
+ try {
364
+ // rendering logic
365
+ } catch (e) {
366
+ console.error('Visual error:', e);
367
+ }
368
+ \`\`\`
369
+
370
+ ## 🎨 Formatting Pane (Modern)
371
+
372
+ 14. **Use FormattingModel**: Implement getFormattingModel() for modern formatting
373
+ - Provides better UX than legacy format pane
374
+ - Required for new visuals
375
+
376
+ ## 🧪 Testing
377
+
378
+ 15. **Unit Tests**: Test data transformation logic
379
+ 16. **Visual Tests**: Use Playwright or similar for E2E tests
380
+ 17. **Test Edge Cases**: Empty data, single point, large datasets
381
+
382
+ ## 📝 Documentation
383
+
384
+ 18. **README.md**: Document visual capabilities and usage
385
+ 19. **Changelog**: Track version changes
386
+ 20. **Inline Comments**: Document complex logic
387
+
388
+ ---
389
+ For more details, visit: https://learn.microsoft.com/en-us/power-bi/developer/visuals/
390
+ `;
391
+ }