@spaced-out/ui-design-system
Version:
Sense UI components library
408 lines (342 loc) ⢠12.7 kB
JavaScript
/**
* Build script to extract design system metadata into JSON
* This runs before publishing to npm to bundle all component/token data
*/
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync } from 'fs';
import { join, resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Design system is parent directory
const DESIGN_SYSTEM_PATH = resolve(__dirname, '..');
const OUTPUT_DIR = join(__dirname, 'data');
const OUTPUT_FILE = join(OUTPUT_DIR, 'design-system.json');
console.log('Building Genesis MCP data...\n');
console.log(`Design System: ${DESIGN_SYSTEM_PATH}`);
console.log(`Output: ${OUTPUT_FILE}\n`);
function safeReadFile(filePath) {
try {
return readFileSync(filePath, 'utf-8');
} catch (error) {
return null;
}
}
function getDirectories(path) {
try {
return readdirSync(path).filter(f => {
const fullPath = join(path, f);
return statSync(fullPath).isDirectory() && !f.startsWith('.');
});
} catch (error) {
console.error(`ā Error reading directories in ${path}:`, error.message);
return [];
}
}
/**
* Get all files recursively in a directory
*/
function getAllFilesRecursively(dirPath, filesList = [], baseDir = dirPath) {
try {
const files = readdirSync(dirPath);
files.forEach(file => {
if (file.startsWith('.')) return;
const filePath = join(dirPath, file);
const stat = statSync(filePath);
if (stat.isDirectory()) {
getAllFilesRecursively(filePath, filesList, baseDir);
} else {
const relativePath = filePath.replace(baseDir + '/', '');
filesList.push(relativePath);
}
});
} catch (error) {
// Ignore errors for non-existent directories
}
return filesList;
}
/**
* Extract all components with their files (including sub-components)
* For main component files and sub-components, we use .d.ts files from lib/
* to avoid exposing source code in the public npm package
*/
function buildComponentsData() {
console.log('š¦ Extracting components...');
const srcComponentsPath = join(DESIGN_SYSTEM_PATH, 'src/components');
const libComponentsPath = join(DESIGN_SYSTEM_PATH, 'lib/components');
const components = {};
if (!existsSync(srcComponentsPath)) {
console.warn('ā ļø Components path not found');
return components;
}
const componentDirs = getDirectories(srcComponentsPath).filter(name => name !== 'index.ts');
for (const componentName of componentDirs) {
const srcComponentDir = join(srcComponentsPath, componentName);
const libComponentDir = join(libComponentsPath, componentName);
// Get all files in the component directory (from src for reference)
const allFiles = getAllFilesRecursively(srcComponentDir);
// Extract main component .d.ts file from lib/ (type definitions only)
const mainDts = join(libComponentDir, `${componentName}.d.ts`);
const mainContent = safeReadFile(mainDts);
// Extract story files (keep from src - useful for usage examples)
const storyTsx = join(srcComponentDir, `${componentName}.stories.tsx`);
const storyTs = join(srcComponentDir, `${componentName}.stories.ts`);
const storyContent = safeReadFile(storyTsx) || safeReadFile(storyTs);
// Extract CSS file (keep from src - needed for styling reference)
const cssFile = join(srcComponentDir, `${componentName}.module.css`);
const cssContent = safeReadFile(cssFile);
// Extract index .d.ts file from lib/
const indexDts = join(libComponentDir, 'index.d.ts');
const indexContent = safeReadFile(indexDts);
// Extract all additional sub-component .d.ts files from lib/
const additionalFiles = {};
const libAllFiles = existsSync(libComponentDir) ? getAllFilesRecursively(libComponentDir) : [];
for (const file of libAllFiles) {
// Skip main files we already extracted
if (
file === `${componentName}.d.ts` ||
file === 'index.d.ts' ||
!file.endsWith('.d.ts') ||
file.endsWith('.d.ts.map')
) {
continue;
}
// Include .d.ts files for sub-components
const fullPath = join(libComponentDir, file);
const content = safeReadFile(fullPath);
if (content) {
additionalFiles[file] = {
path: file,
content: content
};
}
}
components[componentName] = {
name: componentName,
path: `src/components/${componentName}`,
files: {
main: mainContent ? { path: `${componentName}.d.ts`, content: mainContent } : null,
story: storyContent ? { path: `${componentName}.stories.tsx`, content: storyContent } : null,
css: cssContent ? { path: `${componentName}.module.css`, content: cssContent } : null,
index: indexContent ? { path: 'index.d.ts', content: indexContent } : null,
additional: additionalFiles,
},
allFiles,
};
}
console.log(` ā
Extracted ${Object.keys(components).length} components`);
return components;
}
/**
* Extract all hooks
* For main hook files, we use .d.ts files from lib/
* to avoid exposing source code in the public npm package
*/
function buildHooksData() {
console.log('šŖ Extracting hooks...');
const srcHooksPath = join(DESIGN_SYSTEM_PATH, 'src/hooks');
const libHooksPath = join(DESIGN_SYSTEM_PATH, 'lib/hooks');
const hooks = {};
if (!existsSync(srcHooksPath)) {
console.warn('ā ļø Hooks path not found');
return hooks;
}
const hookDirs = getDirectories(srcHooksPath).filter(name => name !== 'index.ts');
for (const hookName of hookDirs) {
const srcHookDir = join(srcHooksPath, hookName);
const libHookDir = join(libHooksPath, hookName);
// Read main hook .d.ts file from lib/ (type definitions only)
const mainDts = join(libHookDir, `${hookName}.d.ts`);
const mainContent = safeReadFile(mainDts);
// Read story file (keep from src - useful for usage examples)
const storyTsx = join(srcHookDir, `${hookName}.stories.tsx`);
const storyTs = join(srcHookDir, `${hookName}.stories.ts`);
const storyContent = safeReadFile(storyTsx) || safeReadFile(storyTs);
// Read index .d.ts file from lib/
const indexDts = join(libHookDir, 'index.d.ts');
const indexContent = safeReadFile(indexDts);
const allFiles = existsSync(srcHookDir)
? readdirSync(srcHookDir).filter(f => !f.startsWith('.'))
: [];
hooks[hookName] = {
name: hookName,
path: `src/hooks/${hookName}`,
files: {
main: mainContent ? { path: `${hookName}.d.ts`, content: mainContent } : null,
story: storyContent ? { path: `${hookName}.stories.tsx`, content: storyContent } : null,
index: indexContent ? { path: 'index.d.ts', content: indexContent } : null,
},
allFiles,
};
}
console.log(` ā
Extracted ${Object.keys(hooks).length} hooks`);
return hooks;
}
/**
* Extract all design tokens
*/
function buildTokensData() {
console.log('šØ Extracting design tokens...');
const tokensPath = join(DESIGN_SYSTEM_PATH, 'design-tokens');
const tokens = {};
if (!existsSync(tokensPath)) {
console.warn('ā ļø Design tokens path not found');
return tokens;
}
const categories = getDirectories(tokensPath);
for (const category of categories) {
tokens[category] = {};
const categoryPath = join(tokensPath, category);
try {
const files = readdirSync(categoryPath).filter(f => f.endsWith('.json'));
for (const file of files) {
const filePath = join(categoryPath, file);
try {
const content = readFileSync(filePath, 'utf-8');
const tokenData = JSON.parse(content);
tokens[category][file.replace('.json', '')] = tokenData;
} catch (error) {
console.warn(` ā ļø Error parsing ${file}:`, error.message);
}
}
} catch (error) {
console.warn(` ā ļø Error reading category ${category}:`, error.message);
}
}
const tokenCount = Object.values(tokens).reduce((sum, cat) => sum + Object.keys(cat).length, 0);
console.log(` ā
Extracted ${tokenCount} token files across ${Object.keys(tokens).length} categories`);
return tokens;
}
/**
* Extract all utils
* For util files, we use .d.ts files from lib/
* to avoid exposing source code in the public npm package
*/
function buildUtilsData() {
console.log('š§ Extracting utils...');
const srcUtilsPath = join(DESIGN_SYSTEM_PATH, 'src/utils');
const libUtilsPath = join(DESIGN_SYSTEM_PATH, 'lib/utils');
const utils = {};
if (!existsSync(srcUtilsPath)) {
console.warn('ā ļø Utils path not found');
return utils;
}
const utilDirs = getDirectories(srcUtilsPath);
for (const utilName of utilDirs) {
const srcUtilDir = join(srcUtilsPath, utilName);
const libUtilDir = join(libUtilsPath, utilName);
// Get all files in the util directory (from src for reference)
const allFiles = getAllFilesRecursively(srcUtilDir);
// Read all .d.ts files from lib/
const files = {};
const libAllFiles = existsSync(libUtilDir) ? getAllFilesRecursively(libUtilDir) : [];
for (const file of libAllFiles) {
// Only include .d.ts files (skip .js and .d.ts.map)
if (file.endsWith('.d.ts') && !file.endsWith('.d.ts.map')) {
const fullPath = join(libUtilDir, file);
const content = safeReadFile(fullPath);
if (content) {
files[file] = {
path: file,
content: content
};
}
}
}
utils[utilName] = {
name: utilName,
path: `src/utils/${utilName}`,
files: files,
allFiles: allFiles,
};
}
console.log(` ā
Extracted ${Object.keys(utils).length} util modules`);
return utils;
}
/**
* Extract all types
*/
function buildTypesData() {
console.log('š Extracting types...');
const typesPath = join(DESIGN_SYSTEM_PATH, 'src/types');
const types = {};
if (!existsSync(typesPath)) {
console.warn('ā ļø Types path not found');
return types;
}
try {
const files = readdirSync(typesPath).filter(f =>
!f.startsWith('.') &&
(f.endsWith('.ts') || f.endsWith('.tsx')) &&
!f.endsWith('.d.ts')
);
for (const file of files) {
const filePath = join(typesPath, file);
const content = safeReadFile(filePath);
if (content) {
const typeName = file.replace(/\.(ts|tsx)$/, '');
types[typeName] = {
name: typeName,
path: `src/types/${file}`,
content: content,
};
}
}
} catch (error) {
console.warn(` ā ļø Error reading types:`, error.message);
}
console.log(` ā
Extracted ${Object.keys(types).length} type files`);
return types;
}
/**
* Get package version
*/
function getVersion() {
try {
const pkgPath = join(DESIGN_SYSTEM_PATH, 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
return pkg.version || '0.0.0';
} catch (error) {
return '0.0.0';
}
}
/**
* Main build function
*/
function build() {
try {
const data = {
metadata: {
buildDate: new Date().toISOString(),
version: getVersion(),
designSystemPath: DESIGN_SYSTEM_PATH,
},
components: buildComponentsData(),
hooks: buildHooksData(),
tokens: buildTokensData(),
utils: buildUtilsData(),
types: buildTypesData(),
};
// Create output directory if it doesn't exist
if (!existsSync(OUTPUT_DIR)) {
mkdirSync(OUTPUT_DIR, { recursive: true });
}
// Write to JSON file
writeFileSync(OUTPUT_FILE, JSON.stringify(data, null, 2));
console.log('\n⨠Build complete!');
console.log(`š Summary:`);
console.log(` - Components: ${Object.keys(data.components).length}`);
console.log(` - Hooks: ${Object.keys(data.hooks).length}`);
console.log(` - Token categories: ${Object.keys(data.tokens).length}`);
console.log(` - Utils: ${Object.keys(data.utils).length}`);
console.log(` - Types: ${Object.keys(data.types).length}`);
console.log(` - Version: ${data.metadata.version}`);
console.log(` - Output: ${OUTPUT_FILE}`);
console.log(` - Size: ${(Buffer.byteLength(JSON.stringify(data)) / 1024 / 1024).toFixed(2)} MB\n`);
} catch (error) {
console.error('ā Build failed:', error);
process.exit(1);
}
}
// Run the build
build();