@salesforce/metadata-visualizer-web
v1.1.0
Published
Web/HTTP SDK for the Salesforce Metadata Visualizer framework. Lets any HTTP server plug a host-supplied IFileSystem into the framework and serve plugin visualizations.
Readme
@salesforce/metadata-visualizer-web
Web/HTTP SDK for the Salesforce Metadata Visualizer framework (@salesforce/metadata-visualizer-core). Lets any HTTP-based host (custom web services, browser applications, demo pages) plug a host-supplied filesystem into the framework and serve plugin visualizations to a browser.
This package mirrors the role visualizer-vscode-ext plays for VS Code: it owns the host-specific plumbing (postMessage bootstrap, plugin asset bundling, @dist/ URL rewriting) and exposes a small façade so consumers never see plugin internals.
Interfaces the host must implement
The SDK is built around the principle of interface inversion: we define small contracts in core-sdk, and each host (browser application, VS Code, others) implements them. The SDK does not impose a transport, a router, or a filesystem layout — it just consumes what the host supplies and returns blobs the host can serve.
There is exactly one interface the host must implement: IFileSystem. The engine also accepts an optional telemetry hook; everything else is handled internally by the SDK.
Required: IFileSystem (from @salesforce/metadata-core-sdk)
This is the only interface the host must implement. It is the host's bridge between the framework and the user-project files the host owns.
Why this is required
Plugin parsers (SchemaParser, FlexipageParser, future plugins) read the user's metadata files through this interface. The host implements it once against its existing I/O layer — delegating to your project service's file operations, which should enforce path traversal guards, restricted-path rules (.git, .sf, node_modules, dotfiles), path length limits, and per-project isolation.
Methods to implement
| Method | What it does | Implementation notes |
| -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| readFile(path) | Read a file as a UTF-8 string. Returns Result<string>. | Delegate to your file I/O layer. Wrap path validation errors into { success: false, error }. |
| writeFile(path, content) | Write a file. Returns Result<void>. | Plugins do not write today; you can throw or stub. |
| exists(path) | Whether a file or directory exists. Returns Promise<boolean>. | Use your project service's path resolution + file system checks. |
| getMetadata(path) | Returns { path, fileName, extension }. | Pure path operations + stat for existence. |
| readDirectory(path) | Lists directory entries with { name, isFile }. | Delegate to your directory listing implementation. |
| findFiles(pattern, maxResults?) | Workspace-wide glob (e.g. **/*.object-meta.xml). | Pattern language is minimatch with POSIX separators, anchored at the workspace root. Schema plugin uses this to discover related objects. |
| getWorkspaceRoot() | Absolute path of the project root. | One project per IFileSystem instance — construct one per request bound to that request's project. |
| normalizePath, separator, basename, dirname, join, extname, parse, relative, resolve, isAbsolute | Pure string operations. | Delegate to Node's path module. |
The full type definition lives at @salesforce/metadata-core-sdk → IFileSystem.
Optional: telemetry (ITelemetry from @salesforce/metadata-core-sdk)
A telemetry hook for tracking usage and errors, passed to createWebVisualizationEngine alongside fileSystem. If the host doesn't supply it, the SDK falls back to a no-op implementation.
What the host gets back from the SDK
After implementing IFileSystem and constructing the engine via createWebVisualizationEngine, the host calls these methods. The WebVisualizationEngine class provides five core methods:
Core API Methods
| Method | Returns | Use Case |
| -------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| getPlugin(metadataFilePath) | PluginInfo \| null | Resolve a metadata file path to its registered plugin (returns id, displayName, etc.) |
| visualizeMetadata(pluginId) | { bundle: HttpAsset<string> \| null, error: PluginError \| null } | Generate a fully self-contained HTML shell for the plugin's UI (no parsing — UI only) |
| getParsedMetadata(metadataFilePath) | VisualizeOutcome — { result, error } | Parse metadata and return structured data without generating HTML |
| getUpdatedParsedMetadata(metadataFilePath) | { error, pluginDataUpdateMessage } | Generate update message for posting to existing plugin iframe (live updates) |
| dispose() | void | Release all resources (call on shutdown) |
Method Details
Two-step flow. UI generation and metadata parsing are separate calls. visualizeMetadata produces the static plugin UI shell (no file I/O, no parsing); the loaded iframe then requests parsed data via postMessage, which the host serves from getParsedMetadata. Use getPlugin(filePath) to resolve a metadata file to its plugin ID before calling visualizeMetadata.
getPlugin(metadataFilePath) — Resolve plugin for a file:
- Matches the file path against registered plugin file patterns
- Returns
PluginInfo(withid) on match,nullotherwise - Synchronous and side-effect-free
visualizeMetadata(pluginId) — Generate UI shell:
- Returns fully self-contained HTML with all assets (JS, CSS, platform styles) inlined
- No metadata parsing occurs — this generates the static plugin UI only
- Zero additional HTTP requests needed
- Perfect for iframe embedding:
<iframe srcdoc="..."></iframe> - The loaded plugin iframe requests its data separately via postMessage
- Error handling: Check
errorfield first, then usebundle
getParsedMetadata(metadataFilePath) — Parse metadata only:
- Returns parsed metadata as structured data
- Use for REST endpoints, data analysis, validation, and as the data source for plugin postMessage requests
- No HTML generation overhead
getUpdatedParsedMetadata(metadataFilePath) — Live updates:
- Parses file and generates
PluginDataUpdateMessageready for iframe postMessage - Use with file watchers to push updates to already-loaded visualizations
- Plugins listen via
onDataUpdatefromHostCommunicationUtils
Integration walkthrough
This section explains how a web host integrates the SDK end-to-end. The actual route plumbing depends on your framework conventions (Express, Fastify, etc.). The shape below shows the integration pattern at the seams.
Step 1 — Implement IFileSystem
The host writes a single class that wraps its existing file I/O layer. Each method delegates to your existing file operations. The class is constructed per request, bound to that request's project root.
// Host implementation — sketch only
class MyHostFileSystem implements IFileSystem {
constructor(private readonly projectDir: string) {}
// readFile, findFiles, exists, getMetadata, readDirectory →
// delegate to your file service
// pure-string helpers (basename, join, etc.) → delegate to node:path
// getWorkspaceRoot() → return this.projectDir
}Step 2 — Construct the engine per request
For each incoming HTTP request that needs visualization:
const fileSystem = new MyHostFileSystem(projectDir);
const engine = await createWebVisualizationEngine({ fileSystem });
// Use the engine, then dispose when the request ends.Per-request construction is acceptable for prototypes because plugin loading is cheap. Production hosts can cache engines per project if profiling shows it matters.
Step 3 — Wire HTTP routes
The host adds visualizer routes to its service using the five core WebVisualizationEngine methods:
Route 1: Generate HTML Bundle
app.post('/visualizer/bundle', async (req, res) => {
const { filePath } = req.body;
// Resolve file → plugin first; visualizeMetadata takes a pluginId, not a path.
const pluginInfo = engine.getPlugin(filePath);
if (!pluginInfo) {
return res.status(415).json({ error: 'No plugin found for this file type' });
}
const { bundle, error } = await engine.visualizeMetadata(pluginInfo.id);
if (error) {
const status = error.category === 'Configuration' ? 415 : 500;
return res.status(status).json({
error: error.message,
category: error.category,
context: error.context,
});
}
res.type(bundle.contentType).set('Cache-Control', bundle.cacheControl).send(bundle.body);
});Use case: Generate the plugin UI shell for iframe embedding. Returns fully self-contained HTML with all assets inlined. Note: this generates the UI only — the loaded iframe requests parsed metadata separately via postMessage, served by the parse route below.
Route 2: Parse Metadata
app.post('/visualizer/parse', async (req, res) => {
const { filePath } = req.body;
const outcome = await engine.getParsedMetadata(filePath);
if (outcome.error) {
return res.status(500).json({
error: outcome.error.message,
category: outcome.error.category,
});
}
res.json({
data: outcome.result.data,
metadata: outcome.result.metadata,
filePath: outcome.result.filePath,
});
});Use case: Data-only endpoint. Plugins request this via postMessage; useful for validation, analysis, and structured data access.
Route 3: Live Updates
app.post('/visualizer/update', async (req, res) => {
const { filePath } = req.body;
const { error, pluginDataUpdateMessage } = await engine.getUpdatedParsedMetadata(filePath);
if (error) {
return res.status(500).json({ error: error.message });
}
// Return message for host to post to iframe
res.json({ updateMessage: pluginDataUpdateMessage, success: true });
});
// File watcher integration (server-side)
fileWatcher.on('change', async (filePath) => {
const { pluginDataUpdateMessage } = await engine.getUpdatedParsedMetadata(filePath);
if (pluginDataUpdateMessage) {
// Send to connected clients via WebSocket
wss.clients.forEach((client) => {
client.send(JSON.stringify(pluginDataUpdateMessage));
});
}
});Use case: Push updates to already-loaded iframe when file content changes. Plugins listen via onDataUpdate:
import { onDataUpdate } from '@salesforce/metadata-core-sdk/utils/HostCommunicationUtils';
useEffect(() => {
const unsubscribe = onDataUpdate((freshData) => {
setData(freshData); // Re-render with updated data
});
return unsubscribe;
}, []);Step 4 — Wire the browser side
The host's UI mounts an <iframe> whose src is the bundle URL above. Because the iframe is same-origin (everything served from your domain), the bootstrap script the SDK injected can postMessage cleanly to the parent window. The parent UI listens for those messages and proxies REQUEST_PLUGIN_DATA back to the parse endpoint, then posts PLUGIN_DATA_RESPONSE back into the iframe.
This relay logic lives entirely on the host's side. The package gives the iframe a working __ExtensionHostPostMessage and an HTML document; the host decides what listens to those messages on the parent.
Architecture rules enforced by tests
The SDK has two strict architectural rules — both checked at test time:
- No runtime dependency on
@salesforce/metadata-plugins. Plugin React builds are bundled into the SDK's owndist/plugins/at build time viascripts/copy-plugin-assets.mjs— same pattern the VS Code adapter uses with webpackCopyPlugin. The plugins package is referenced only at the workspace root'sdevDependencies. Consumers'npm installnever pulls plugins in directly. - No
vscodeimport. This is a browser-host SDK; VS Code-flavored code stays invisualizer-vscode-ext.
Both are enforced by src/__tests__/architecture.test.ts.
Development
npm run build # tsc + scripts/copy-plugin-assets.mjs (populates dist/plugins/, dist/design-system/)
npm test # jest with coverage
npm run typecheck # tsc --noEmit
npm run lintBuild runs from the workspace root — @salesforce/metadata-plugins lives in the root's devDependencies and the build script resolves it from there. Building the package outside the workspace is not supported.
Public API summary
Core Class
WebVisualizationEngine— Main class with five methods:getPlugin(metadataFilePath)— Resolve a file path to its registered pluginvisualizeMetadata(pluginId)— Generate the plugin UI shell (HTML bundle, no parsing)getParsedMetadata(metadataFilePath)— Parse metadata onlygetUpdatedParsedMetadata(metadataFilePath)— Generate update message for live updatesdispose()— Release resources
createWebVisualizationEngine(opts)— Async factory function
Supporting Types
HttpAsset<T>— Response wrapper:{ contentType, body, cacheControl }PluginInfo— Plugin metadata:{ id, displayName, filePatterns, priority? }VisualizeOutcome— Parse result:{ result, error }. Theerrorfield is aPluginErrorfrom@salesforce/metadata-core-sdk.PluginDataUpdateMessage<T = unknown>— Update message for postMessage:{ type: 'PLUGIN_DATA_UPDATE', payload: PluginDataResponse<T> }. GenericTlets plugin code with a known data shape opt into type-safety; consumers can narrow onmessage.typein a discriminated-union switch.WebAdapterOptions— Engine constructor options:{ fileSystem, telemetry? }
License
BSD-3-Clause.
