UNPKG

@adobe/spectrum-component-diff-generator

Version:

Generates diff reports for Spectrum component schema changes with breaking change analysis

383 lines (352 loc) 13.2 kB
/* Copyright 2024 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Component-specific file import utilities using shared core import { FileLoader, buildFileURL, buildFetchOptions, cleanFilePath, processFileNames as processGenericFileNames, } from "@adobe/spectrum-diff-core"; // Component-specific constants and configuration const COMPONENT_PACKAGE_PATH = "packages/component-schemas"; // ===== COMPONENT-SPECIFIC WRAPPER FUNCTIONS ===== /** * Constructs the GitHub URL for fetching component schemas * @param {string} fileName - Name of the schema file * @param {string} version - Package version or "latest" * @param {string} location - Branch name when version is "latest" * @param {string} repo - Repository in format "owner/repo" * @returns {string} Complete URL for the schema file */ export function buildComponentURL(fileName, version, location, repo) { return buildFileURL( fileName, version, location, repo, COMPONENT_PACKAGE_PATH, ); } /** * Cleans and normalizes component schema file path * @param {string} startDir - Base directory path * @param {string} fileName - Raw file name from input * @returns {string} Cleaned file path */ export function cleanComponentPath(startDir, fileName) { return cleanFilePath(startDir, fileName); } /** * Processes component schema file names, adding "schemas/components/" prefix if needed * @param {Array<string>} fileNames - Array of schema file names * @param {boolean} hasGivenFileNames - Whether file names were explicitly provided * @returns {Array<string>} Processed file names */ export function processComponentFileNames(fileNames, hasGivenFileNames) { return processGenericFileNames( fileNames, hasGivenFileNames, "schemas/components/", ); } // Re-export shared functions for backward compatibility export { buildFetchOptions }; // ===== COMPONENT-SPECIFIC CLASSES USING SHARED CORE ===== /** * Component-specific file loader using shared core */ export class ComponentLoader extends FileLoader { constructor() { // Configure shared core for component-specific paths and settings super(); } /** * Loads component schemas from remote repository * @param {Array<string>} givenFileNames - Schema file names to load * @param {string} givenVersion - Package version * @param {string} givenLocation - Branch name * @param {string} givenRepo - Repository name * @param {string} githubAPIKey - API key for authentication * @returns {Promise<object>} Merged schema data */ async loadRemoteComponents( givenFileNames, givenVersion, givenLocation, givenRepo, githubAPIKey, ) { // If specific files provided, use them; otherwise discover all available files let fileNames = givenFileNames; if (!fileNames) { // Dynamically discover component schema files from the remote branch fileNames = await this.discoverRemoteComponentFiles( givenVersion || "latest", givenLocation || "main", givenRepo, githubAPIKey, ); } // Load individual component files and key by component name const result = {}; for (const fileName of fileNames) { try { const componentData = await this.remoteFetcher.fetchFile( fileName, givenVersion || "latest", givenLocation || "main", givenRepo, COMPONENT_PACKAGE_PATH, githubAPIKey, ); // Use the filename without extension as the component key const componentName = fileName.split("/").pop().replace(".json", ""); result[componentName] = componentData; } catch (error) { // File might not exist in this branch - this is expected for new/deleted components console.debug( `Debug: Could not load component file ${fileName}: ${error.message}`, ); } } return result; } /** * Discovers all component schema files from a remote branch * @param {string} version - Package version or "latest" * @param {string} location - Branch name * @param {string} repo - Repository name * @param {string} githubAPIKey - API key for authentication * @returns {Promise<Array<string>>} List of discovered component file paths */ async discoverRemoteComponentFiles(version, location, repo, githubAPIKey) { try { // Try to fetch the GitHub API directory listing const repoName = repo || "adobe/spectrum-tokens"; const branch = version === "latest" ? location : version; const apiUrl = `https://api.github.com/repos/${repoName}/contents/packages/component-schemas/schemas`; const options = githubAPIKey ? { headers: { Authorization: `token ${githubAPIKey}` } } : {}; const response = await fetch(`${apiUrl}?ref=${branch}`, options); if (response.ok) { const contents = await response.json(); const files = []; // Process directory contents recursively for (const item of contents) { if (item.type === "file" && item.name.endsWith(".json")) { files.push(`schemas/${item.name}`); } else if (item.type === "dir") { // Recursively get files from subdirectories (like components/, types/) const subFiles = await this.getFilesFromDirectory( `${apiUrl}/${item.name}`, branch, options, `schemas/${item.name}`, ); files.push(...subFiles); } } return files; } else { console.warn( `Could not discover files via GitHub API (${response.status}), using fallback list`, ); return this.getFallbackComponentFiles(); } } catch (error) { console.warn( `Could not discover files via GitHub API: ${error.message}, using fallback list`, ); return this.getFallbackComponentFiles(); } } /** * Gets files from a directory via GitHub API * @param {string} dirUrl - API URL for the directory * @param {string} branch - Branch name * @param {object} options - Fetch options * @param {string} pathPrefix - Path prefix to prepend to file names * @returns {Promise<Array<string>>} List of file paths */ async getFilesFromDirectory(dirUrl, branch, options, pathPrefix) { try { const response = await fetch(`${dirUrl}?ref=${branch}`, options); if (response.ok) { const contents = await response.json(); return contents .filter((item) => item.type === "file" && item.name.endsWith(".json")) .map((item) => `${pathPrefix}/${item.name}`); } return []; } catch (error) { console.debug(`Could not list directory ${dirUrl}: ${error.message}`); return []; } } /** * Returns fallback list of component files when dynamic discovery fails * @returns {Array<string>} Fallback file list */ getFallbackComponentFiles() { return [ "schemas/component.json", "schemas/components/action-bar.json", "schemas/components/action-button.json", "schemas/components/action-group.json", "schemas/components/alert-banner.json", "schemas/components/alert-dialog.json", "schemas/components/avatar.json", "schemas/components/badge.json", "schemas/components/body.json", "schemas/components/bottom-navigation-android.json", "schemas/components/breadcrumbs.json", "schemas/components/button-group.json", "schemas/components/button.json", "schemas/components/checkbox-group.json", "schemas/components/checkbox.json", "schemas/components/close-button.json", "schemas/components/coach-indicator.json", "schemas/components/code.json", "schemas/components/color-area.json", "schemas/components/color-loupe.json", "schemas/components/color-slider.json", "schemas/components/color-wheel.json", "schemas/components/combo-box.json", "schemas/components/contextual-help.json", "schemas/components/detail.json", "schemas/components/divider.json", "schemas/components/field-label.json", "schemas/components/heading.json", "schemas/components/help-text.json", "schemas/components/in-field-progress-button.json", "schemas/components/in-field-progress-circle.json", "schemas/components/in-line-alert.json", "schemas/components/link.json", "schemas/components/menu.json", "schemas/components/meter.json", "schemas/components/opacity-checkerboard.json", "schemas/components/picker.json", "schemas/components/popover.json", "schemas/components/progress-bar.json", "schemas/components/progress-circle.json", "schemas/components/radio-button.json", "schemas/components/radio-group.json", "schemas/components/rating.json", "schemas/components/scroll-zoom-bar.json", "schemas/components/search-field.json", "schemas/components/select-box.json", "schemas/components/side-navigation.json", "schemas/components/slider.json", "schemas/components/status-light.json", "schemas/components/swatch-group.json", "schemas/components/swatch.json", "schemas/components/switch.json", "schemas/components/tab-bar-ios.json", "schemas/components/tabs.json", "schemas/components/tag.json", "schemas/components/text-area.json", "schemas/components/text-field.json", "schemas/components/thumbnail.json", "schemas/components/toast.json", "schemas/components/tooltip.json", "schemas/components/tray.json", "schemas/components/tree-view.json", "schemas/types/hex-color.json", "schemas/types/typography-classification.json", "schemas/types/typography-script.json", "schemas/types/workflow-icon.json", ]; } /** * Gets list of local component files * @param {string} dirName - Directory containing schema files * @returns {Promise<Array<string>>} List of component file paths */ async getLocalComponentFiles(dirName) { try { const allFiles = await this.localFS.getFiles(dirName, "*.json"); return allFiles.map((filePath) => { // Convert absolute paths to relative schema paths const relativePath = filePath.replace(dirName + "/", ""); return relativePath.startsWith("schemas/") ? relativePath : `schemas/${relativePath}`; }); } catch (error) { console.warn( `Warning: Could not discover local component files: ${error.message}`, ); return []; } } /** * Loads component schemas from local file system * @param {string} dirName - Directory containing schema files * @param {Array<string>} fileNames - Specific files to load (optional) * @returns {Promise<object>} Merged schema data */ async loadLocalComponents(dirName, fileNames) { // Load all files using shared core const allFiles = await this.localFS.getFiles(dirName, "*.json"); const result = {}; for (const filePath of allFiles) { try { const fileData = await this.localFS.loadData([filePath]); // Extract component name from file path const componentName = filePath.split("/").pop().replace(".json", ""); result[componentName] = fileData; } catch (error) { console.warn( `Warning: Could not load local component file ${filePath}: ${error.message}`, ); } } return result; } } // ===== BACKWARD COMPATIBILITY EXPORTS ===== // Create default instances const defaultComponentLoader = new ComponentLoader(); /** * Main entry point for loading component schemas * @param {Array<string>} givenFileNames - Schema file names * @param {string} givenVersion - Package version * @param {string} givenLocation - Branch name * @param {string} givenRepo - Repository name * @param {string} githubAPIKey - API key * @returns {Promise<object>} Schema data */ export default async function componentFileImport( givenFileNames, givenVersion, givenLocation, givenRepo, githubAPIKey, ) { return await defaultComponentLoader.loadRemoteComponents( givenFileNames, givenVersion, givenLocation, givenRepo, githubAPIKey, ); } /** * Loads local component schema data * @param {string} dirName - Directory name * @param {Array<string>} fileNames - Schema file names * @returns {Promise<object>} Schema data */ export async function loadLocalComponentData(dirName, fileNames) { return await defaultComponentLoader.loadLocalComponents(dirName, fileNames); }