gluestack-ui
Version:
A CLI tool for easily adding components from gluestack to your projects.
379 lines (378 loc) • 19.1 kB
JavaScript
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;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "fs-extra", "path", "@clack/prompts", "../../config", "../config"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.modifyLayoutFile = modifyLayoutFile;
exports.modifyLayoutFilesAutomatically = modifyLayoutFilesAutomatically;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const prompts_1 = require("@clack/prompts");
const config_1 = require("../../config");
const config_2 = require("../config");
/**
* Automatically modifies layout files to add GluestackUIProvider wrapper and CSS imports
*/
function modifyLayoutFile(options) {
return __awaiter(this, void 0, void 0, function* () {
const { layoutPath, projectType, cssPath, componentsPath, isNextjs15 } = options;
try {
if (!fs.existsSync(layoutPath)) {
prompts_1.log.warning(`Layout file not found: ${layoutPath}`);
return;
}
const content = yield fs.readFile(layoutPath, 'utf8');
// Check if already modified
if (isLayoutAlreadyWrapped(content)) {
prompts_1.log.info(`Layout file already contains GluestackUIProvider setup: ${path.basename(layoutPath)}`);
return;
}
const isTypeScript = layoutPath.endsWith('.tsx') || layoutPath.endsWith('.ts');
let modifiedContent;
if (projectType === 'nextjs') {
modifiedContent = yield modifyNextJsLayout(content, cssPath, componentsPath, isTypeScript, isNextjs15);
}
else if (projectType === 'expo') {
modifiedContent = yield modifyExpoLayout(content, cssPath, componentsPath, isTypeScript);
}
else if (projectType === 'react-native-cli') {
modifiedContent = yield modifyReactNativeLayout(content, cssPath, componentsPath, isTypeScript);
}
else {
prompts_1.log.warning(`Unsupported project type for layout modification: ${projectType}`);
return;
}
if (modifiedContent !== content) {
yield fs.writeFile(layoutPath, modifiedContent, 'utf8');
prompts_1.log.success(`✅ Modified layout file: ${path.basename(layoutPath)}`);
}
}
catch (error) {
prompts_1.log.error(`Error modifying layout file: ${error.message}`);
throw error;
}
});
}
/**
* Check if layout is already wrapped with GluestackUIProvider
*/
function isLayoutAlreadyWrapped(content) {
return (content.includes('GluestackUIProvider') &&
(content.includes('<GluestackUIProvider') ||
content.includes('GluestackUIProvider>')) &&
content.includes('gluestack-ui-provider'));
}
/**
* Modify Next.js layout files (app router or pages router)
*/
function modifyNextJsLayout(content, cssPath, componentsPath, isTypeScript, isNextjs15) {
return __awaiter(this, void 0, void 0, function* () {
const cssImportPath = getCssImportPath(cssPath);
const providerImportPath = `@/${componentsPath}/gluestack-ui-provider`;
// Determine if this is App Router or Pages Router
const isAppRouter = content.includes('export default function RootLayout') ||
content.includes('export default function Layout');
const isPagesRouter = content.includes('export default function App');
// For Next.js 15, only import the registry for App Router
const registryImport = isNextjs15 && isAppRouter
? `import StyledJsxRegistry from './registry';`
: '';
let imports = [
`import '${cssImportPath}';`,
`import { GluestackUIProvider } from '${providerImportPath}';`,
registryImport,
]
.filter(Boolean)
.join('\n');
// Add imports at the top (after existing imports)
let modifiedContent = addImportsToFile(content, imports);
// Wrap the children with provider (only if not already wrapped)
if (isAppRouter) {
// App Router layout
const providerWrapper = isNextjs15
? `<StyledJsxRegistry>\n <GluestackUIProvider mode="dark">\n {children}\n </GluestackUIProvider>\n </StyledJsxRegistry>`
: `<GluestackUIProvider mode="dark">\n {children}\n </GluestackUIProvider>`;
modifiedContent = modifiedContent.replace(/(\s*{children}\s*)/g, `\n ${providerWrapper}\n `);
}
else if (isPagesRouter) {
// Pages Router _app.tsx - no StyledJsxRegistry needed here
modifiedContent = modifiedContent.replace(/(<Component\s+{\.\.\.pageProps}\s*\/>)/g, `<GluestackUIProvider mode="dark">\n $1\n </GluestackUIProvider>`);
}
return modifiedContent;
});
}
/**
* Modify Expo layout files (_layout.tsx)
*/
function modifyExpoLayout(content, cssPath, componentsPath, isTypeScript) {
return __awaiter(this, void 0, void 0, function* () {
const cssImportPath = getCssImportPath(cssPath);
const providerImportPath = `@/${componentsPath}/gluestack-ui-provider`;
const imports = [
`import { GluestackUIProvider } from '${providerImportPath}';`,
`import '${cssImportPath}';`,
].join('\n');
// Add imports at the top
let modifiedContent = addImportsToFile(content, imports);
// Wrap the return content with provider
if (content.includes('<Slot />')) {
modifiedContent = modifiedContent.replace(/(\s*<Slot\s*\/>\s*)/g, `\n <GluestackUIProvider mode="dark">\n <Slot />\n </GluestackUIProvider>\n `);
}
else if (content.includes('return (')) {
// Find the main return statement and wrap its content
modifiedContent = modifiedContent.replace(/(return\s*\(\s*)([\s\S]*?)(\s*\);?\s*})/, (match, returnPart, content, closingPart) => {
if (content.trim().startsWith('<GluestackUIProvider')) {
return match; // Already wrapped
}
return `${returnPart}\n <GluestackUIProvider mode="dark">\n ${content.trim()}\n </GluestackUIProvider>\n ${closingPart}`;
});
}
return modifiedContent;
});
}
/**
* Modify React Native CLI layout files (App.tsx)
*/
function modifyReactNativeLayout(content, cssPath, componentsPath, isTypeScript) {
return __awaiter(this, void 0, void 0, function* () {
const cssImportPath = getCssImportPath(cssPath);
const providerImportPath = `@/${componentsPath}/gluestack-ui-provider`;
const imports = [
`import { GluestackUIProvider } from '${providerImportPath}';`,
`import '${cssImportPath}';`,
].join('\n');
// Add imports at the top
let modifiedContent = addImportsToFile(content, imports);
// Wrap the main App component return
if (content.includes('function App') || content.includes('const App')) {
modifiedContent = modifiedContent.replace(/(return\s*\(\s*)([\s\S]*?)(\s*\);?\s*})/, (match, returnPart, content, closingPart) => {
if (content.trim().startsWith('<GluestackUIProvider')) {
return match; // Already wrapped
}
return `${returnPart}\n <GluestackUIProvider mode="dark">\n ${content.trim()}\n </GluestackUIProvider>\n ${closingPart}`;
});
}
return modifiedContent;
});
}
/**
* Enhanced addImportsToFile function to prevent duplicate imports
*/
function addImportsToFile(content, newImports) {
// Split new imports into individual import statements
const newImportLines = newImports.split('\n').filter(line => line.trim());
// Find existing imports
const importRegex = /^import\s+.*?;?\s*$/gm;
const existingImports = content.match(importRegex) || [];
// Check which imports already exist to avoid duplicates
const importsToAdd = [];
newImportLines.forEach(newImport => {
const newImportTrimmed = newImport.trim();
// Skip empty lines
if (!newImportTrimmed)
return;
// Extract the import path for comparison
const importPathMatch = newImportTrimmed.match(/from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/);
const importPath = importPathMatch ? (importPathMatch[1] || importPathMatch[2]) : '';
// For CSS imports, check the import path directly - FIXED REGEX
const isCssImport = newImportTrimmed.match(/^import\s+['"][^'"]*\.(css|scss|sass|less)['"];?\s*$/);
// Extract imported items for more precise matching (only for non-CSS imports)
const importItemsMatch = newImportTrimmed.match(/import\s+(.+?)\s+from/);
const importItems = importItemsMatch ? importItemsMatch[1].trim() : '';
// Check if this import already exists
const isDuplicate = existingImports.some(existingImport => {
const existingPathMatch = existingImport.match(/from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/);
const existingPath = existingPathMatch ? (existingPathMatch[1] || existingPathMatch[2]) : '';
// For CSS imports, just compare paths
if (isCssImport) {
const existingIsCssImport = existingImport.match(/^import\s+['"][^'"]*\.(css|scss|sass|less)['"];?\s*$/);
return existingIsCssImport && existingPath === importPath;
}
const existingItemsMatch = existingImport.match(/import\s+(.+?)\s+from/);
const existingItems = existingItemsMatch ? existingItemsMatch[1].trim() : '';
// Check if same path and similar import structure
return existingPath === importPath && (existingItems === importItems ||
(importItems.includes('GluestackUIProvider') && existingItems.includes('GluestackUIProvider')) ||
(importItems.includes('StyledJsxRegistry') && existingItems.includes('StyledJsxRegistry')));
});
if (!isDuplicate) {
importsToAdd.push(newImportTrimmed);
}
});
// If no new imports to add, return original content
if (importsToAdd.length === 0) {
return content;
}
if (existingImports.length > 0) {
// Find the position after the last import
const lastImport = existingImports[existingImports.length - 1];
const lastImportIndex = content.indexOf(lastImport) + lastImport.length;
// Insert new imports after the last existing import
return (content.slice(0, lastImportIndex) +
'\n' +
importsToAdd.join('\n') +
'\n' +
content.slice(lastImportIndex));
}
else {
// No existing imports, add at the top
return importsToAdd.join('\n') + '\n' + content;
}
}
/**
* Get the correct CSS import path based on the CSS file location
*/
function getCssImportPath(cssPath) {
if (cssPath.includes('global')) {
return '@/' + cssPath;
}
else if (cssPath.startsWith('src/')) {
return '@/' + cssPath;
}
else {
return '@/' + cssPath;
}
}
/**
* Automatically modifies layout files during init process
*/
function modifyLayoutFilesAutomatically(projectType, resolvedConfig, permission) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
if (!permission) {
prompts_1.log.info('Skipping layout file modification due to user preference.');
return;
}
try {
// Use the resolved config to get the actual entry path
const entryPath = (_a = resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.app) === null || _a === void 0 ? void 0 : _a.entry;
const componentsPath = ((_b = resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.app) === null || _b === void 0 ? void 0 : _b.componentsPath) ||
((_c = resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.app) === null || _c === void 0 ? void 0 : _c.components) ||
'components/ui';
if (!entryPath) {
prompts_1.log.warning('No entry path found in resolved config. Skipping layout modification.');
return;
}
// Determine CSS path based on project type
const cssPath = yield getCSSPathForProject(projectType, resolvedConfig);
// Check if this is Next.js 15
const isNextjs15 = projectType === 'nextjs' && (yield checkForNextjs15());
const options = {
layoutPath: entryPath,
projectType,
cssPath,
componentsPath,
isNextjs15,
};
prompts_1.log.info(`🔄 Automatically modifying layout file: ${path.basename(entryPath)}`);
yield modifyLayoutFile(options);
}
catch (error) {
prompts_1.log.error(`Failed to automatically modify layout files: ${error.message}`);
prompts_1.log.info('You can manually add the GluestackUIProvider wrapper to your layout file.');
}
});
}
/**
* Get CSS path based on project type and resolved config
*/
function getCSSPathForProject(projectType, resolvedConfig) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
if ((_a = resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.app) === null || _a === void 0 ? void 0 : _a.globalCssPath) {
return resolvedConfig.app.globalCssPath;
}
// Try to detect existing CSS files in the project
const detectedCssPath = yield (0, config_2.getFilePath)([
'**/*globals.css',
'**/*global.css',
]);
if (detectedCssPath) {
return detectedCssPath;
}
// Fallback to default CSS paths for different project types
switch (projectType) {
case config_1.config.nextJsProject:
return 'app/globals.css';
case config_1.config.expoProject:
return 'global.css';
case config_1.config.reactNativeCLIProject:
return 'global.css';
default:
return 'global.css';
}
});
}
/**
* Check if the project is using Next.js 15
*/
function checkForNextjs15() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
try {
const packageJsonPath = path.join(process.cwd(), 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return false;
}
const packageJson = JSON.parse(yield fs.readFile(packageJsonPath, 'utf8'));
const nextVersion = ((_a = packageJson.dependencies) === null || _a === void 0 ? void 0 : _a.next) || ((_b = packageJson.devDependencies) === null || _b === void 0 ? void 0 : _b.next);
if (nextVersion) {
// Check if version starts with 15 or ^15 or ~15
return /^[\^~]?15\./.test(nextVersion);
}
return false;
}
catch (error) {
return false;
}
});
}
});