UNPKG

sb-mig

Version:

CLI to rule the world. (and handle stuff related to Storyblok CMS)

369 lines (368 loc) 13.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.loadResourceContent = loadResourceContent; exports.loadResources = loadResources; exports.discoverComponents = discoverComponents; exports.discoverDatasources = discoverDatasources; exports.discoverRoles = discoverRoles; const promises_1 = require("fs/promises"); const path_1 = require("path"); const url_1 = require("url"); /** * Load the content of a resource file (.sb.js, .datasource.js, etc.) * Uses dynamic import to load ES modules and CommonJS */ async function loadResourceContent(filePath) { try { // Use dynamic import which works for both ESM and CJS const fileUrl = (0, url_1.pathToFileURL)(filePath).href; const module = await Promise.resolve(`${fileUrl}`).then(s => __importStar(require(s))); return module.default || module; } catch (error) { throw new Error(`Failed to load ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } /** * Load multiple resources by file path */ async function loadResources(filePaths) { const results = []; for (const filePath of filePaths) { const name = filePath .split("/") .pop() ?.replace(/\.sb\.(js|cjs|mjs|ts)$/, "") .replace(/\.(datasource|roles)\.(js|cjs|ts)$/, "") .replace(/\.sb\.(datasource|roles)\.(js|cjs|ts)$/, "") || "unknown"; try { const data = await loadResourceContent(filePath); results.push({ name, filePath, data }); } catch (error) { results.push({ name, filePath, data: null, error: error instanceof Error ? error.message : String(error), }); } } return results; } /** * Read componentsDirectories from storyblok.config.js if it exists */ async function readComponentDirectories(workingDir) { const configFiles = [ "storyblok.config.js", "storyblok.config.cjs", "storyblok.config.mjs", ]; for (const configFile of configFiles) { try { const configPath = (0, path_1.join)(workingDir, configFile); const configContent = await (0, promises_1.readFile)(configPath, "utf-8"); const match = configContent.match(/componentsDirectories\s*:\s*\[([\s\S]*?)\]/); if (match && match[1]) { const dirsMatch = match[1].match(/['"]([^'"]+)['"]/g); if (dirsMatch) { return dirsMatch.map((d) => d.replace(/['"]/g, "")); } } break; } catch { // Config file doesn't exist, continue } } return ["src", "components", "storyblok"]; } /** * Check if a path is within the project directory * Resolves symlinks and ensures we don't escape the project bounds */ async function isWithinProject(targetPath, projectRoot) { try { // Resolve both paths to handle symlinks const resolvedTarget = await (0, promises_1.realpath)(targetPath); const resolvedRoot = await (0, promises_1.realpath)(projectRoot); // Use the platform separator so this works on Windows (`\`) and POSIX (`/`). return (resolvedTarget.startsWith(resolvedRoot + path_1.sep) || resolvedTarget === resolvedRoot); } catch { // If we can't resolve the path, assume it's not safe return false; } } /** * Discover components in the working directory * Prefers .ts for local files and .cjs for external (node_modules) files * to avoid duplicates when both ESM and CJS versions exist * * Security: Stays within project bounds and doesn't follow symlinks outside */ async function discoverComponents(workingDir, options) { const components = []; // Priority order: .ts first (local), then .cjs (for node_modules) // Skip .js and .mjs to avoid duplicates const extensions = options?.extensions ?? [".sb.ts", ".sb.cjs"]; const includeExternal = options?.includeExternal ?? true; const maxDepth = options?.maxDepth ?? 20; // Resolve the project root for security checks const projectRoot = (0, path_1.resolve)(workingDir); const componentDirs = await readComponentDirectories(workingDir); const scanDir = async (dir, isExternal, depth) => { // Prevent excessive depth if (depth > maxDepth) { return; } // Security: Ensure we're still within project bounds if (!(await isWithinProject(dir, projectRoot))) { return; } try { const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = (0, path_1.join)(dir, entry.name); if (entry.isDirectory()) { // Skip common non-source directories if (entry.name === ".git" || entry.name === ".next" || entry.name === "dist" || entry.name === ".cache" || entry.name === "coverage") { continue; } // Skip node_modules entirely if not including external if (entry.name === "node_modules" && !includeExternal) { continue; } const isNowExternal = isExternal || entry.name === "node_modules"; await scanDir(fullPath, isNowExternal, depth + 1); } else if (entry.isFile()) { // Skip external files if not including them if (isExternal && !includeExternal) { continue; } for (const ext of extensions) { if (entry.name.endsWith(ext) && !entry.name.startsWith("_")) { const componentName = entry.name.replace(ext, ""); components.push({ name: componentName, filePath: fullPath, type: isExternal ? "external" : "local", }); break; } } } } } catch { // Directory doesn't exist or can't be read } }; for (const dir of componentDirs) { const fullDir = (0, path_1.join)(workingDir, dir); // Skip if the directory path includes node_modules and we're not including external if (dir.includes("node_modules") && !includeExternal) { continue; } try { const dirStat = await (0, promises_1.stat)(fullDir); if (dirStat.isDirectory()) { await scanDir(fullDir, dir.includes("node_modules"), 0); } } catch { // Directory doesn't exist } } // Also scan root try { const rootEntries = await (0, promises_1.readdir)(workingDir, { withFileTypes: true }); for (const entry of rootEntries) { if (entry.isFile()) { for (const ext of extensions) { if (entry.name.endsWith(ext) && !entry.name.startsWith("_")) { const componentName = entry.name.replace(ext, ""); if (!components.find((c) => c.name === componentName)) { components.push({ name: componentName, filePath: (0, path_1.join)(workingDir, entry.name), type: "local", }); } break; } } } } } catch { // Ignore } // Deduplicate: prefer .ts over .cjs for same component name const seen = new Map(); for (const component of components) { const existing = seen.get(component.name); if (!existing) { seen.set(component.name, component); } else { // Prefer .ts files over .cjs if (component.filePath.endsWith(".ts") && !existing.filePath.endsWith(".ts")) { seen.set(component.name, component); } // Prefer local over external else if (component.type === "local" && existing.type === "external") { seen.set(component.name, component); } } } const deduplicated = Array.from(seen.values()); // Sort: local first, then by name deduplicated.sort((a, b) => { if (a.type !== b.type) { return a.type === "local" ? -1 : 1; } return a.name.localeCompare(b.name); }); return deduplicated; } /** * Discover datasources in the working directory */ async function discoverDatasources(workingDir) { const datasources = []; const extensions = [ ".datasource.js", ".datasource.cjs", ".sb.datasource.js", ".sb.datasource.cjs", ]; const scanDir = async (dir) => { try { const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = (0, path_1.join)(dir, entry.name); if (entry.isDirectory()) { if (entry.name === ".git" || entry.name === ".next" || entry.name === "dist" || entry.name === "node_modules") { continue; } await scanDir(fullPath); } else if (entry.isFile()) { for (const ext of extensions) { if (entry.name.endsWith(ext) && !entry.name.startsWith("_")) { const name = entry.name .replace(ext, "") .replace(".sb", ""); datasources.push({ name, filePath: fullPath, type: "local", }); break; } } } } } catch { // Skip } }; await scanDir(workingDir); datasources.sort((a, b) => a.name.localeCompare(b.name)); return datasources; } /** * Discover roles in the working directory */ async function discoverRoles(workingDir) { const roles = []; const extensions = [".sb.roles.js", ".sb.roles.cjs", ".sb.roles.ts"]; const scanDir = async (dir) => { try { const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = (0, path_1.join)(dir, entry.name); if (entry.isDirectory()) { if (entry.name === ".git" || entry.name === ".next" || entry.name === "dist" || entry.name === "node_modules") { continue; } await scanDir(fullPath); } else if (entry.isFile()) { for (const ext of extensions) { if (entry.name.endsWith(ext) && !entry.name.startsWith("_")) { const name = entry.name.replace(ext, ""); roles.push({ name, filePath: fullPath, type: "local", }); break; } } } } } catch { // Skip } }; await scanDir(workingDir); roles.sort((a, b) => a.name.localeCompare(b.name)); return roles; }