UNPKG

create-netsuite-react-app

Version:

CLI tool to create a production-ready NetSuite React TypeScript app with full API integration

298 lines (254 loc) 11.3 kB
import fs from 'fs-extra'; import path from 'path'; import { fileURLToPath } from 'url'; import chalk from 'chalk'; import inquirer from 'inquirer'; import ora from 'ora'; import { checkDirectoryExists, createDirectoryIfNotExists, copyTemplateFiles, replaceInFile } from './utils.js'; // Get __dirname equivalent in ESM const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const DEFAULT_PROJECT_NAME = 'ns_react_app_template'; const DEFAULT_DISPLAY_NAME = 'NetSuite React App Template'; export async function createApp(projectDirectory, options = {}) { const { verbose = false } = options; // Create the absolute path for the new project const targetDir = path.resolve(process.cwd(), projectDirectory); // Check if target directory exists and if it's not empty const dirCheck = checkDirectoryExists(targetDir); if (dirCheck.exists && !dirCheck.empty) { const { proceed } = await inquirer.prompt({ type: 'confirm', name: 'proceed', message: `Directory ${projectDirectory} already exists and is not empty. Continue?`, default: false }); if (!proceed) { console.log(chalk.yellow('Operation cancelled.')); process.exit(0); } } // Create the directory if it doesn't exist createDirectoryIfNotExists(targetDir); // Get project configuration details const answers = await inquirer.prompt([ { type: 'input', name: 'displayName', message: 'Enter display name for your app:', default: projectDirectory.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') }, { type: 'input', name: 'idPrependValue', message: 'Enter ID prepend value (optional, e.g., "edr" → customscript_edr_project_name):', default: '', validate: (input) => { if (!input) return true; // Allow empty if (!/^[a-z0-9]+$/.test(input)) { return 'Prepend value must be lowercase letters and numbers only.'; } return true; } } ]); const { displayName, idPrependValue } = answers; // Try different paths to find the template directory let templateDir; const possibleTemplatePaths = [ path.resolve(__dirname, '../../template'), // Local development path path.resolve(__dirname, '../template'), // Published package path path.resolve(__dirname, '../../../template'), // Global installed path path.join(process.cwd(), 'node_modules', 'create-netsuite-react-app', 'template') // Installed in node_modules ]; for (const potentialPath of possibleTemplatePaths) { if (verbose) { console.log(`Checking for template at: ${potentialPath}`); } if (fs.existsSync(potentialPath)) { templateDir = potentialPath; if (verbose) { console.log(`Found template at: ${templateDir}`); } break; } } if (!templateDir) { console.error(chalk.red('Error: Template directory not found.')); console.error(chalk.yellow('Searched in:')); possibleTemplatePaths.forEach(p => console.error(chalk.yellow(`- ${p}`))); throw new Error('Template directory not found. The package may be corrupted.'); } // Copy the template to the target directory const copySpinner = ora('Copying template files...').start(); try { await copyTemplateFiles(templateDir, targetDir); // Ensure .gitignore is created (npm may ignore it during packaging) const gitignoreTarget = path.join(targetDir, '.gitignore'); const gitignoreTemplate = path.join(templateDir, 'gitignore.template'); const gitignoreSource = path.join(templateDir, '.gitignore'); if (!fs.existsSync(gitignoreTarget)) { if (fs.existsSync(gitignoreSource)) { await fs.copy(gitignoreSource, gitignoreTarget); } else if (fs.existsSync(gitignoreTemplate)) { await fs.copy(gitignoreTemplate, gitignoreTarget); } } copySpinner.succeed('Template files copied successfully'); } catch (error) { copySpinner.fail(`Failed to copy template: ${error.message || error}`); throw error; } // Update project configuration const updateSpinner = ora('Updating project configuration...').start(); try { // Create basic project.json for SuiteCloud (without custom fields to avoid conflicts) const configPath = path.join(targetDir, 'project.json'); const projectConfig = { defaultAuthId: "" }; if (verbose) { console.log(`Writing basic project.json for SuiteCloud:`, projectConfig); } try { await fs.writeFile( configPath, JSON.stringify(projectConfig, null, 2) ); if (verbose) { console.log(`✓ Created project.json at ${configPath}`); } } catch (error) { console.error(`❌ Error writing project.json:`, error); throw error; } // Update files with placeholders const filesToUpdate = [ { path: path.join(targetDir, 'reactApp', 'index.html'), replacements: [ ['<title>.*?</title>', `<title>${displayName}</title>`] ] }, { path: path.join(targetDir, 'reactApp', 'src', 'App.tsx'), replacements: [ [/<h1>.*?<\/h1>/, `<h1>${displayName}</h1>`] ] }, { path: path.join(targetDir, 'package.json'), replacements: [ ['"name":\\s*".*?"', `"name": "${projectDirectory}"`], ['"description":\\s*".*?"', `"description": "${displayName}"`], // Add idPrependValue if provided ...(idPrependValue ? [['"license":\\s*".*?"', `"idPrependValue": "${idPrependValue}",\n "license": "ISC"`]] : []) ] }, { path: path.join(targetDir, 'vite.config.ts'), replacements: [ [/const buildRootFolderPath = '\.\/FileCabinet\/SuiteScripts\/.*?';/, `const buildRootFolderPath = './FileCabinet/SuiteScripts/${projectDirectory}';`] ] }, // Update NetSuite manifest with the project name { path: path.join(targetDir, 'manifest.xml'), replacements: [ [/<projectname>.*?<\/projectname>/, `<projectname>${projectDirectory}</projectname>`] ] }, // Update script deployment files to use the new project name { path: path.join(targetDir, 'Objects', 'customscript_ns_react_app_template.xml'), replacements: (() => { const scriptIdBase = idPrependValue ? `${idPrependValue}_${projectDirectory}` : projectDirectory; return [ [/customscript_ns_react_app_template/g, `customscript_${scriptIdBase}`], [/customdeploy_ns_react_app_template/g, `customdeploy_${scriptIdBase}`], [/NS React App Template/g, displayName], // Fixed: Changed from "NetSuite React App Template" [/\/SuiteScripts\/ns_react_app_template\//g, `/SuiteScripts/${projectDirectory}/`] ]; })() } ]; for (const file of filesToUpdate) { const updated = await replaceInFile(file.path, file.replacements); if (verbose) { console.log(`${updated ? '✓' : '⚠'} ${file.path}: ${updated ? 'Updated' : 'No changes made'}`); // For package.json, verify the content after replacement if (file.path.includes('package.json') && updated) { try { const verifyContent = await fs.readFile(file.path, 'utf8'); const packageData = JSON.parse(verifyContent); console.log(`✓ Verified package.json name: "${packageData.name}", description: "${packageData.description}"`); } catch (error) { console.error(`❌ Error verifying package.json:`, error); } } } } // Rename Object XML file to match the new project name (with prepend if provided) const scriptIdBase = idPrependValue ? `${idPrependValue}_${projectDirectory}` : projectDirectory; const oldXmlPath = path.join(targetDir, 'Objects', 'customscript_ns_react_app_template.xml'); const newXmlPath = path.join(targetDir, 'Objects', `customscript_${scriptIdBase}.xml`); if (fs.existsSync(oldXmlPath)) { await fs.rename(oldXmlPath, newXmlPath); } // Rename SuiteScript directories const oldScriptDir = path.join(targetDir, 'FileCabinet', 'SuiteScripts', 'ns_react_app_template'); const newScriptDir = path.join(targetDir, 'FileCabinet', 'SuiteScripts', projectDirectory); if (fs.existsSync(oldScriptDir)) { await fs.rename(oldScriptDir, newScriptDir); // Update the react_app_handler.js file content (keep the same filename) const handlerPath = path.join(newScriptDir, 'react_app_handler.js'); if (fs.existsSync(handlerPath)) { // Update references inside the file let handlerContent = fs.readFileSync(handlerPath, 'utf8'); handlerContent = handlerContent.replace(/FRONTEND_APP_FOLDER_NAME: ['"].*?['"]/, `FRONTEND_APP_FOLDER_NAME: '${projectDirectory}'`); handlerContent = handlerContent.replace(/FRONTEND_APP_TITLE: ['"].*?['"]/, `FRONTEND_APP_TITLE: '${displayName}'`); handlerContent = handlerContent.replace(/FRONTEND_APP_ID: ['"].*?['"]/, `FRONTEND_APP_ID: '${projectDirectory}'`); handlerContent = handlerContent.replace(/const TAG = ['"].*?['"]/, `const TAG = '${projectDirectory.toUpperCase()}'`); fs.writeFileSync(handlerPath, handlerContent); } // Update any template scripts in the scripts directory to use the project name const scriptsDir = path.join(newScriptDir, 'scripts'); const libDir = path.join(newScriptDir, 'lib'); const configDir = path.join(newScriptDir, 'config'); // Update constants.js with project-specific values const constantsPath = path.join(configDir, 'constants.js'); if (fs.existsSync(constantsPath)) { let constantsContent = fs.readFileSync(constantsPath, 'utf8'); constantsContent = constantsContent.replace(/APP_NAME: 'ns_react_app_template'/, `APP_NAME: '${projectDirectory}'`); fs.writeFileSync(constantsPath, constantsContent); } } updateSpinner.succeed('Project configuration updated successfully'); } catch (error) { updateSpinner.fail(`Failed to update project configuration: ${error.message || error}`); throw error; } // Success message const scriptIdBase = idPrependValue ? `${idPrependValue}_${projectDirectory}` : projectDirectory; console.log(` ${chalk.green('Success!')} Created ${projectDirectory} at ${targetDir} ${chalk.yellow('✓')} .gitignore file included for proper version control ${chalk.yellow('✓')} NetSuite project structure ready for deployment ${chalk.yellow('✓')} React development environment configured ${idPrependValue ? chalk.yellow(`✓ Script IDs will use prepend: customscript_${scriptIdBase}`) : ''} We suggest that you begin by typing: ${chalk.cyan(`cd ${projectDirectory}`)} ${chalk.cyan('npm install')} ${chalk.cyan('npm run dev')} To configure your project, run: ${chalk.cyan('npm run setup')} ${chalk.red('IMPORTANT:')} Before deploying to NetSuite: ${chalk.cyan('npm run build')} - Build the React app first! Happy coding! `); }