From a06deda77a8029c0ef457ade0c4083710f981cb6 Mon Sep 17 00:00:00 2001 From: Johnny Hausman Date: Tue, 11 Nov 2025 15:20:04 +0700 Subject: [PATCH 1/4] [wip] initial plugin commands --- .editorconfig | 2 +- lib/oldPlugin.js | 115 ++++ lib/plugin.js | 107 +++ lib/tasks/pluginNew.js | 220 +++++++ lib/tasks/pluginObject.js | 97 +++ lib/tasks/pluginPlatformGenerator.js | 613 ++++++++++++++++++ lib/tasks/pluginPlatformProperties.js | 208 ++++++ lib/tasks/pluginPlatformService.js | 235 +++++++ lib/tasks/pluginPlatformWeb.js | 201 ++++++ lib/utils/pluginDirectoryName.js | 33 + templates/_pluginService_serviceUmd.js | 14 + templates/plugin/.eslintrc.js | 55 ++ templates/plugin/.gitignore | 26 + templates/plugin/README.md | 4 + templates/plugin/index.js | 16 + templates/plugin/manifest.json | 8 + templates/plugin/package.json | 38 ++ templates/plugin/webpack.common.js | 91 +++ templates/plugin/webpack.dev.js | 33 + templates/plugin/webpack.prod.js | 32 + .../properties/FN[objectNamePascal].js | 114 ++++ .../service/FN[objectNamePascal].js | 42 ++ .../service/FN[objectNamePascal]Model.js | 94 +++ .../pluginObject/web/FN[objectNamePascal].js | 69 ++ .../pluginPlatformProperties/properties.js | 8 + templates/pluginPlatformService/service.js | 8 + templates/pluginPlatformWeb/web.js | 8 + 27 files changed, 2490 insertions(+), 1 deletion(-) create mode 100644 lib/oldPlugin.js create mode 100644 lib/plugin.js create mode 100644 lib/tasks/pluginNew.js create mode 100644 lib/tasks/pluginObject.js create mode 100644 lib/tasks/pluginPlatformGenerator.js create mode 100644 lib/tasks/pluginPlatformProperties.js create mode 100644 lib/tasks/pluginPlatformService.js create mode 100644 lib/tasks/pluginPlatformWeb.js create mode 100644 lib/utils/pluginDirectoryName.js create mode 100644 templates/_pluginService_serviceUmd.js create mode 100644 templates/plugin/.eslintrc.js create mode 100644 templates/plugin/.gitignore create mode 100644 templates/plugin/README.md create mode 100644 templates/plugin/index.js create mode 100644 templates/plugin/manifest.json create mode 100644 templates/plugin/package.json create mode 100644 templates/plugin/webpack.common.js create mode 100644 templates/plugin/webpack.dev.js create mode 100644 templates/plugin/webpack.prod.js create mode 100644 templates/pluginObject/properties/FN[objectNamePascal].js create mode 100644 templates/pluginObject/service/FN[objectNamePascal].js create mode 100644 templates/pluginObject/service/FN[objectNamePascal]Model.js create mode 100644 templates/pluginObject/web/FN[objectNamePascal].js create mode 100644 templates/pluginPlatformProperties/properties.js create mode 100644 templates/pluginPlatformService/service.js create mode 100644 templates/pluginPlatformWeb/web.js diff --git a/.editorconfig b/.editorconfig index 237b80a..48b2f9d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,7 +17,7 @@ root = true [*] indent_style = space -indent_size = 4 +indent_size = 3 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true diff --git a/lib/oldPlugin.js b/lib/oldPlugin.js new file mode 100644 index 0000000..eb33944 --- /dev/null +++ b/lib/oldPlugin.js @@ -0,0 +1,115 @@ +// +// oldPlugin +// manage plugin related tasks (old architecture) +// +// options: +// appbuilder oldPlugin new [name] : create a new plugin +// appbuilder oldPlugin page new [name] : create a new page within a plugin +// +var async = require("async"); +var path = require("path"); +var utils = require(path.join(__dirname, "utils", "utils")); + +var oldPluginNew = require(path.join(__dirname, "tasks", "oldPluginNew.js")); +var oldPluginPage = require(path.join(__dirname, "tasks", "oldPluginPage.js")); + +var Options = {}; // the running options for this command. + +// +// Build the Service Command +// +var Command = new utils.Resource({ + command: "oldPlugin", + params: "", + descriptionShort: "manage plugins (old architecture).", + descriptionLong: ` +`, +}); + +module.exports = Command; + +Command.help = function () { + console.log(` + + usage: $ appbuilder oldPlugin [operation] [options] + + [operation]s : + new : $ appbuilder oldPlugin new [name] + page: $ appbuilder oldPlugin page [plugin-name] [page-name] + + + [options] : + name: the name of the plugin + + examples: + + $ appbuilder oldPlugin new RemoteConnection + - creates new plugin in developer/plugins/RemoteConnection +`); +}; + +Command.run = function (options) { + return new Promise((resolve, reject) => { + async.series( + [ + // copy our passed in options to our Options + (done) => { + for (var o in options) { + Options[o] = options[o]; + } + Options.operation = options._.shift(); + + // check for valid params: + if (!Options.operation) { + Command.help(); + process.exit(1); + } + done(); + }, + checkDependencies, + chooseTask, + ], + (err) => { + if (err) { + reject(err); + return; + } + resolve(); + } + ); + }); +}; + +/** + * @function checkDependencies + * verify the system has any required dependencies for our operations. + * @param {function} done node style callback(err) + */ +function checkDependencies(done) { + utils.checkDependencies(["git"], done); +} + +/** + * @function chooseTask + * choose the proper subTask to perform. + * @param {cb(err)} done + */ +function chooseTask(done) { + var task; + switch (Options.operation.toLowerCase()) { + case "new": + task = oldPluginNew; + break; + + case "page": + task = oldPluginPage; + break; + } + if (!task) { + Command.help(); + process.exit(1); + } + + task.run(Options).then(done).catch(done); +} + diff --git a/lib/plugin.js b/lib/plugin.js new file mode 100644 index 0000000..8478653 --- /dev/null +++ b/lib/plugin.js @@ -0,0 +1,107 @@ +// +// plugin +// manage plugin related tasks (updated architecture) +// +var path = require("path"); +var utils = require(path.join(__dirname, "utils", "utils")); + +var pluginNew = require(path.join(__dirname, "tasks", "pluginNew.js")); +var pluginObject = require(path.join(__dirname, "tasks", "pluginObject.js")); + +var Options = {}; // the running options for this command. + +// +// Build the Plugin Command +// +var Command = new utils.Resource({ + command: "plugin", + params: "", + descriptionShort: "manage plugins (updated architecture).", + descriptionLong: ` +`, +}); + +module.exports = Command; + +Command.help = function () { + console.log(` + + usage: $ appbuilder plugin [operation] [options] + + [operation]s : + new : $ appbuilder plugin new [name] + object : $ appbuilder plugin object [pluginName] [objectName] + + [options] : + name: the name of the plugin + + examples: + + $ appbuilder plugin new MyPlugin + - creates new plugin in developer/plugins/ab_plugin_my_plugin + + $ appbuilder plugin object MyPlugin ObjectNetsuite + - adds object code (properties, service, and web) to existing plugin with object name + $ appbuilder plugin object MyPlugin + - adds object code, objectName defaults to pluginName + $ appbuilder plugin object + - prompts to select plugin and object name +`); +}; + +Command.run = async function (options) { + // copy our passed in options to our Options + for (var o in options) { + Options[o] = options[o]; + } + Options.operation = options._.shift(); + + // check for valid params: + if (!Options.operation) { + Command.help(); + process.exit(1); + } + + await checkDependencies(); + await chooseTask(); +}; + +/** + * @function checkDependencies + * verify the system has any required dependencies for our operations. + * @returns {Promise} + */ +function checkDependencies() { + return new Promise((resolve, reject) => { + utils.checkDependencies([], (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +/** + * @function chooseTask + * choose the proper subTask to perform. + * @returns {Promise} + */ +async function chooseTask() { + var task; + switch (Options.operation.toLowerCase()) { + case "new": + task = pluginNew; + break; + case "object": + task = pluginObject; + break; + } + if (!task) { + Command.help(); + process.exit(1); + } + + await task.run(Options); +} diff --git a/lib/tasks/pluginNew.js b/lib/tasks/pluginNew.js new file mode 100644 index 0000000..40ea8e8 --- /dev/null +++ b/lib/tasks/pluginNew.js @@ -0,0 +1,220 @@ +// +// pluginNew +// create a new plugin in the working directory (updated architecture). +// +// options: +// +// +var fs = require("fs"); +var inquirer = require("inquirer"); +var path = require("path"); +var shell = require("shelljs"); +var utils = require(path.join(__dirname, "..", "utils", "utils")); + +var Options = {}; // the running options for this command. + +// +// Build the PluginNew Command +// +var Command = new utils.Resource({ + command: "pluginNew", + params: "", + descriptionShort: + "create a new plugin in the working directory (updated architecture).", + descriptionLong: ` +`, +}); + +module.exports = Command; + +Command.help = function () { + console.log(` + + usage: $ appbuilder plugin new [name] [options] + + create a new plugin in developer/plugins/ + + Options: + [name] the name of the plugin to create. + +`); +}; + +Command.run = async function (options) { + // copy our passed in options to our Options + for (var o in options) { + Options[o] = options[o]; + } + + Options.name = Options._.shift(); + if (!Options.name) { + console.log("missing parameter [name]"); + Command.help(); + process.exit(1); + } + + await checkDependencies(); + await questions(); + await createPluginDirectory(); + await copyTemplateFiles(); +}; + +/** + * @function checkDependencies + * verify the system has any required dependencies for generating plugins. + * @returns {Promise} + */ +function checkDependencies() { + return new Promise((resolve, reject) => { + utils.checkDependencies([], (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +/** + * @function questions + * Present the user with a list of configuration questions. + * If the answer for a question is already present in Options, then we + * skip that question. + * @returns {Promise} + */ +function questions() { + return new Promise((resolve, reject) => { + inquirer + .prompt([ + { + name: "description", + type: "input", + message: "Describe this plugin :", + default: "A new AppBuilder plugin", + when: (values) => { + return ( + !values.description && + typeof Options.description == "undefined" + ); + }, + }, + { + name: "author", + type: "input", + message: "Enter your name (name of the author) :", + default: "AppBuilder Developer", + when: (values) => { + return !values.author && typeof Options.author == "undefined"; + }, + }, + { + name: "icon", + type: "input", + message: "Enter the fontawesome icon reference (fa-*) :", + default: "fa-puzzle-piece", + when: (values) => { + return !values.icon && typeof Options.icon == "undefined"; + }, + }, + ]) + .then((answers) => { + for (var a in answers) { + Options[a] = answers[a]; + } + resolve(); + }) + .catch(reject); + }); +} + +/** + * @function createPluginDirectory + * create the plugin directory with inferred name + * @returns {Promise} + */ +function createPluginDirectory() { + return new Promise((resolve, reject) => { + var pluginDirectoryName = require(path.join( + __dirname, + "..", + "utils", + "pluginDirectoryName" + )); + + // Convert name to directory name format: ab_plugin_ + Options.pluginDirName = pluginDirectoryName(Options.name); + + // Create developer/plugins directory if it doesn't exist + var pluginsDir = path.join(process.cwd(), "developer", "plugins"); + if (!fs.existsSync(pluginsDir)) { + fs.mkdirSync(pluginsDir, { recursive: true }); + } + + var pluginDir = path.join(pluginsDir, Options.pluginDirName); + + // Check if directory already exists + if (fs.existsSync(pluginDir)) { + reject( + new Error( + `Directory developer/plugins/${Options.pluginDirName} already exists. Please choose a different name.` + ) + ); + return; + } + + // Create the directory + fs.mkdirSync(pluginDir, { recursive: true }); + console.log( + `... created directory: developer/plugins/${Options.pluginDirName}` + ); + + resolve(); + }); +} + +/** + * @function copyTemplateFiles + * copy our template files into the project + * @returns {Promise} + */ +function copyTemplateFiles() { + return new Promise((resolve, reject) => { + // Convert name to different formats for templating + var nameNoSpaces = Options.name.replaceAll(" ", ""); + + // pluginName: PascalCase (e.g., "NetsuiteAPI" from "netsuite_api" or "Netsuite API") + var parts = nameNoSpaces.split(/[_-]/); + for (var p = 0; p < parts.length; p++) { + parts[p] = + parts[p].charAt(0).toUpperCase() + parts[p].slice(1).toLowerCase(); + } + Options.pluginName = parts.join(""); + + // pluginKey: snake_case version (e.g., "object_netsuite_api") + Options.pluginKey = nameNoSpaces.toLowerCase().replace(/[^a-z0-9]/g, "_"); + + // pluginNameDisplay: Display name (e.g., "Netsuite API") + Options.pluginNameDisplay = Options.name; + + // Change into the plugin directory + var pluginDir = path.join( + process.cwd(), + "developer", + "plugins", + Options.pluginDirName + ); + shell.pushd("-q", pluginDir); + + utils.fileCopyTemplates("plugin", Options, [], (err) => { + // Change back to original directory + shell.popd("-q"); + + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} diff --git a/lib/tasks/pluginObject.js b/lib/tasks/pluginObject.js new file mode 100644 index 0000000..ef2cd13 --- /dev/null +++ b/lib/tasks/pluginObject.js @@ -0,0 +1,97 @@ +// +// pluginObject +// add object code (properties, service, and web) to an existing plugin +// +// options: +// +// +var path = require("path"); +var utils = require(path.join(__dirname, "..", "utils", "utils")); +var generator = require(path.join(__dirname, "pluginPlatformGenerator")); + +var Options = {}; // the running options for this command. + +// +// Build the PluginObject Command +// +var Command = new utils.Resource({ + command: "pluginObject", + params: "", + descriptionShort: + "add object code (properties, service, and web) to an existing plugin (updated architecture).", + descriptionLong: ` +`, +}); + +module.exports = Command; + +Command.help = function () { + console.log(` + + usage: $ appbuilder plugin object [pluginName] [objectName] [options] + + add object code (properties, service, and web) to a plugin in developer/plugins/ + If the plugin doesn't exist, it will be created first. + + Options: + [pluginName] the name of the plugin to add object code to (will prompt if not provided). + [objectName] the object name for the object (e.g., "ObjectNetsuite" creates FNObjectNetsuite.js). Defaults to pluginName if omitted. + +`); +}; + +Command.run = async function (options) { + // copy our passed in options to our Options + for (var o in options) { + Options[o] = options[o]; + } + + Options.pluginName = Options._.shift(); + Options.objectName = Options._.shift(); + // Object always has type = "object" + Options.type = "object"; + + await generator.checkDependencies(); + + // Check if plugin exists, create it if it doesn't + await generator.ensurePluginExists(Options); + + await generator.findPluginDirectory(Options, "object"); + await generator.questions(Options, "object"); + + // Copy all template files once (this will copy properties/, service/, web/ directories and .js files) + await generator.copyTemplateFiles(Options, "pluginObject"); + + // Call each platform command to handle webpack and manifest updates + // They will skip template copying since files already exist, but will update platform.js, webpack, and manifest + // Prepare options for each command with proper _ array values + + // Properties command expects: [pluginName, objectName] and sets type = "properties" + var propertiesOptions = generator.prepareCommandOptions(Options, [ + Options.pluginName, + Options.objectName, + ]); + var propertiesCommand = require(path.join( + __dirname, + "pluginPlatformProperties" + )); + await propertiesCommand.run(propertiesOptions); + + // Service command expects: [pluginName, type, objectName] + var serviceOptions = generator.prepareCommandOptions(Options, [ + Options.pluginName, + Options.type, + Options.objectName, + ]); + var serviceCommand = require(path.join(__dirname, "pluginPlatformService")); + await serviceCommand.run(serviceOptions); + + // Web command expects: [pluginName, type, objectName] + var webOptions = generator.prepareCommandOptions(Options, [ + Options.pluginName, + Options.type, + Options.objectName, + ]); + var webCommand = require(path.join(__dirname, "pluginPlatformWeb")); + await webCommand.run(webOptions); +}; diff --git a/lib/tasks/pluginPlatformGenerator.js b/lib/tasks/pluginPlatformGenerator.js new file mode 100644 index 0000000..57c594e --- /dev/null +++ b/lib/tasks/pluginPlatformGenerator.js @@ -0,0 +1,613 @@ +// +// pluginPlatformGenerator +// Shared code for pluginPlatformService and pluginPlatformWeb +// +var fs = require("fs"); +var inquirer = require("inquirer"); +var path = require("path"); +var shell = require("shelljs"); +var utils = require(path.join(__dirname, "..", "utils", "utils")); + +/** + * @function checkDependencies + * verify the system has any required dependencies for generating plugins. + * @returns {Promise} + */ +function checkDependencies() { + return new Promise((resolve, reject) => { + utils.checkDependencies([], (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +/** + * @function findPluginDirectory + * find the plugin directory based on the plugin name, or let user choose + * @param {object} Options - the options object containing pluginName + * @param {string} platformName - name of platform for user messages (e.g., "service", "web") + * @returns {Promise} + */ +function findPluginDirectory(Options, platformName) { + return new Promise((resolve, reject) => { + var pluginsDir = path.join(process.cwd(), "developer", "plugins"); + + if (!fs.existsSync(pluginsDir)) { + reject( + new Error( + `Directory developer/plugins/ does not exist. Please create a plugin first.` + ) + ); + return; + } + + // If pluginName is provided, try to find it + if (Options.pluginName) { + var pluginDirectoryName = require(path.join( + __dirname, + "..", + "utils", + "pluginDirectoryName" + )); + + // Try to find plugin directory - could be exact match or ab_plugin_* format + var expectedDirName = pluginDirectoryName(Options.pluginName); + var possibleDirs = [ + Options.pluginName, + expectedDirName, + // Also try lowercase version for backwards compatibility + Options.pluginName.toLowerCase().replace(/[^a-z0-9]/g, "_"), + ]; + var foundDir = null; + var pluginFiles = fs.readdirSync(pluginsDir); + for (var pf in pluginFiles) { + var pluginFilePath = path.join(pluginsDir, pluginFiles[pf]); + if (fs.statSync(pluginFilePath).isDirectory()) { + if ( + pluginFiles[pf] === Options.pluginName || + pluginFiles[pf].toLowerCase() === + Options.pluginName.toLowerCase() || + possibleDirs.indexOf(pluginFiles[pf]) !== -1 + ) { + foundDir = pluginFiles[pf]; + break; + } + } + } + + if (!foundDir) { + reject( + new Error( + `Plugin directory not found for "${Options.pluginName}". Please check the plugin name.` + ) + ); + return; + } + + Options.pluginDir = path.join(pluginsDir, foundDir); + Options.pluginDirName = foundDir; + console.log( + `... found plugin directory: developer/plugins/${foundDir}` + ); + resolve(); + } else { + // Scan plugins directory and let user choose + var files = fs.readdirSync(pluginsDir); + var pluginDirs = []; + for (var f in files) { + var filePath = path.join(pluginsDir, files[f]); + if (fs.statSync(filePath).isDirectory()) { + // Try to get plugin name from manifest + var manifestPath = path.join(filePath, "manifest.json"); + var displayName = files[f]; + if (fs.existsSync(manifestPath)) { + try { + var manifest = JSON.parse( + fs.readFileSync(manifestPath, "utf8") + ); + if (manifest.name) { + displayName = `${manifest.name} (${files[f]})`; + } + } catch (e) { + // Ignore manifest parsing errors + } + } + pluginDirs.push({ + name: displayName, + value: files[f], + }); + } + } + + if (pluginDirs.length === 0) { + reject( + new Error( + `No plugins found in developer/plugins/. Please create a plugin first.` + ) + ); + return; + } + + inquirer + .prompt([ + { + name: "selectedPlugin", + type: "list", + message: `Select the plugin to add ${platformName} code to:`, + choices: pluginDirs, + }, + ]) + .then((answers) => { + Options.pluginDir = path.join( + pluginsDir, + answers.selectedPlugin + ); + Options.pluginDirName = answers.selectedPlugin; + console.log( + `... selected plugin: developer/plugins/${answers.selectedPlugin}` + ); + resolve(); + }) + .catch(reject); + } + }); +} + +/** + * @function questions + * Present the user with a list of configuration questions. + * @param {object} Options - the options object + * @param {string} platformName - name of platform for user messages (e.g., "service", "web") + * @returns {Promise} + */ +function questions(Options, platformName) { + return new Promise((resolve, reject) => { + // Helper function to process object name + function processObjectName() { + // Ensure objectName is PascalCase + var objectNameNoSpaces = Options.objectName.replaceAll(" ", ""); + var objectParts = objectNameNoSpaces.split(/[_-]/); + for (var p = 0; p < objectParts.length; p++) { + objectParts[p] = + objectParts[p].charAt(0).toUpperCase() + + objectParts[p].slice(1).toLowerCase(); + } + Options.objectNamePascal = objectParts.join(""); + + // Create FN[ObjectName] format + Options.fnObjectName = "FN" + Options.objectNamePascal; + + // Generate plugin key: "ab-object-[objectname-lowercase-with-hyphens]" + // Convert PascalCase to kebab-case: "ObjectNetsuiteAPI" -> "object-netsuite-api" + var pluginKey = Options.objectNamePascal + .replace(/([a-z])([A-Z])/g, "$1-$2") // lowercase followed by uppercase + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2") // sequence of capitals followed by capital+lowercase + .toLowerCase() // Convert to lowercase + .replace(/[^a-z0-9-]/g, "-") // Replace non-alphanumeric (except hyphens) with hyphen + .replace(/-+/g, "-") // Replace multiple hyphens with single hyphen + .replace(/^-|-$/g, ""); // Remove leading/trailing hyphens + Options.pluginKey = `ab-object-${pluginKey}`; + } + + // Load plugin manifest to get pluginName + var manifestPath = path.join(Options.pluginDir, "manifest.json"); + if (fs.existsSync(manifestPath)) { + try { + var manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + // Extract pluginName from manifest or directory name + Options.pluginName = + manifest.name || + Options.pluginDirName.replace(/^ab_plugin_/, ""); + } catch (e) { + // If manifest parsing fails, use directory name + Options.pluginName = Options.pluginDirName.replace( + /^ab_plugin_/, + "" + ); + } + } else { + Options.pluginName = Options.pluginDirName.replace(/^ab_plugin_/, ""); + } + + // Default objectName to pluginName if not provided + if (!Options.objectName) { + Options.objectName = Options.pluginName; + } + + // Handle type and objectName - prompt if not provided + var prompts = []; + if (!Options.type) { + prompts.push({ + name: "type", + type: "input", + message: `Enter the type for the ${platformName} (e.g., 'object', 'model'):`, + default: "object", + validate: (input) => { + if (!input || input.trim() === "") { + return "Type is required"; + } + return true; + }, + }); + } + // Only prompt for objectName if it wasn't provided (still equals pluginName) + // This allows user to override the default + if (Options.objectName === Options.pluginName) { + prompts.push({ + name: "objectName", + type: "input", + message: `Enter the object name for the ${platformName} (e.g., 'ObjectNetsuite'):`, + default: Options.pluginName, + validate: (input) => { + if (!input || input.trim() === "") { + return "Object name is required"; + } + return true; + }, + }); + } + + if (prompts.length > 0) { + inquirer + .prompt(prompts) + .then((answers) => { + if (answers.type) Options.type = answers.type; + if (answers.objectName) Options.objectName = answers.objectName; + if (Options.objectName) processObjectName(); + resolve(); + }) + .catch(reject); + } else { + if (Options.objectName) processObjectName(); + resolve(); + } + }); +} + +/** + * @function updatePlatformJs + * Update existing platform.js (service.js or web.js) to add new import and array entry + * @param {string} platformJsPath path to platform.js file + * @param {object} Options - the options object + * @param {string} subDir - subdirectory name ("service", "web" or "properties") + * @returns {Promise} + */ +function updatePlatformJs(platformJsPath, Options, subDir) { + return new Promise((resolve, reject) => { + try { + var contents = fs.readFileSync(platformJsPath, "utf8"); + + // Check if this fnObjectName is already imported + var importPattern = new RegExp( + `import\\s+${Options.fnObjectName}\\s+from\\s+["'].*${Options.fnObjectName}["']`, + "i" + ); + if (importPattern.test(contents)) { + console.log( + `... ${Options.fnObjectName} already exists in ${subDir}.js` + ); + resolve(); + return; + } + + // Add the new import statement + var newImport = `import ${Options.fnObjectName} from "./${subDir}/${Options.fnObjectName}.js";`; + + // Find where to insert the import (after existing imports, before export) + // Look for the last import statement or export + var lastImportPattern = /(import\s+[^;]+;)/g; + var matches = []; + var match; + while ((match = lastImportPattern.exec(contents)) !== null) { + matches.push(match); + } + + if (matches.length > 0) { + // Insert after the last import + var lastMatch = matches[matches.length - 1]; + var insertPos = lastMatch.index + lastMatch[0].length; + contents = + contents.substring(0, insertPos) + + "\n" + + newImport + + contents.substring(insertPos); + } else { + // No imports found, insert before export or at the beginning + var exportPattern = /export\s+default\s+function/; + if (exportPattern.test(contents)) { + contents = contents.replace(exportPattern, newImport + "\n\n$&"); + } else { + // If no export found, prepend to file + contents = newImport + "\n\n" + contents; + } + } + + // Find the return array and add the new entry + // Look for: return [ ... ]; + var returnArrayPattern = /return\s*\[\s*([^\]]*)\s*\]/s; + if (returnArrayPattern.test(contents)) { + var returnMatch = contents.match(returnArrayPattern); + var existingItems = returnMatch[1].trim(); + var newItem = `${Options.fnObjectName}(PluginAPI)`; + + // Check if item already exists in array + if (existingItems.indexOf(Options.fnObjectName) !== -1) { + console.log( + `... ${Options.fnObjectName} already exists in return array` + ); + resolve(); + return; + } + + // Add new item to array + var newArrayContent; + if (existingItems) { + // Clean up existing items (remove extra whitespace/newlines) + var cleanedItems = existingItems + .split(",") + .map((item) => item.trim()) + .filter((item) => item.length > 0) + .join(",\n "); + // Add comma and new item + newArrayContent = cleanedItems + ",\n " + newItem; + } else { + // First item in array + newArrayContent = newItem; + } + + contents = contents.replace( + returnArrayPattern, + `return [\n ${newArrayContent}\n ]` + ); + } else { + // No return array found, try to add it + var returnPattern = /return\s+[^[;]+/; + if (returnPattern.test(contents)) { + // Replace single return with array + contents = contents.replace( + returnPattern, + `return [\n ${Options.fnObjectName}(PluginAPI)\n ]` + ); + } else { + // Try to find the function body and add return + var functionBodyPattern = + /(export\s+default\s+function\s+\w+\s*\([^)]*\)\s*\{[^}]*)(\})/s; + if (functionBodyPattern.test(contents)) { + contents = contents.replace( + functionBodyPattern, + `$1\n return [\n ${Options.fnObjectName}(PluginAPI)\n ];\n$2` + ); + } else { + reject( + new Error( + `Could not parse ${subDir}.js to add new entry. Please update manually.` + ) + ); + return; + } + } + } + + fs.writeFileSync(platformJsPath, contents); + console.log(`... updated ${subDir}.js with ${Options.fnObjectName}`); + resolve(); + } catch (err) { + reject(err); + } + }); +} + +/** + * @function updateManifest + * Update manifest.json to add platform entry to .plugins array + * @param {object} Options - the options object + * @param {string} platform - platform name ("service" or "web") + * @param {string} platformPath - the path to the platform file (e.g., "./MyPlugin_web.mjs") + * @param {function} uniquenessCheck - function to check if entry already exists + * @returns {Promise} + */ +function updateManifest(Options, platform, platformPath, uniquenessCheck) { + return new Promise((resolve, reject) => { + var manifestPath = path.join(Options.pluginDir, "manifest.json"); + + if (!fs.existsSync(manifestPath)) { + reject( + new Error( + `manifest.json not found in plugin directory: ${Options.pluginDir}` + ) + ); + return; + } + + var manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + + // Initialize plugins array if it doesn't exist + if (!manifest.plugins) { + manifest.plugins = []; + } + + // Check uniqueness using provided function + if (uniquenessCheck && uniquenessCheck(manifest.plugins)) { + console.log(`... ${platform} entry already exists in manifest.json`); + resolve(); + return; + } + + // Add new platform entry + var platformKey = + Options.pluginName.toLowerCase().replace(/[^a-z0-9]/g, "_") + + `_${platform}`; + var platformEntry = { + name: + Options.pluginName + + " " + + platform.charAt(0).toUpperCase() + + platform.slice(1), + key: platformKey, + platform: platform, + type: Options.type || "object", + path: platformPath, + }; + + manifest.plugins.push(platformEntry); + + // Write back to file with proper formatting + fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 3) + "\n"); + console.log(`... updated manifest.json with ${platform} entry`); + resolve(); + }); +} + +/** + * @function copyTemplateFiles + * copy all template files from a template directory into the plugin directory + * @param {object} Options - the options object containing pluginDir + * @param {string} templateDir - the template directory name (e.g., "pluginObject", "pluginPlatformService") + * @returns {Promise} + */ +function copyTemplateFiles(Options, templateDir) { + return new Promise((resolve, reject) => { + // Change into the plugin directory + shell.pushd("-q", Options.pluginDir); + + // Use fileCopyTemplates to copy all template files from the specified template folder + utils.fileCopyTemplates(templateDir, Options, [], (err) => { + shell.popd("-q"); + if (err) { + reject(err); + return; + } + resolve(); + }); + }); +} + +/** + * @function ensurePluginExists + * Check if the plugin exists, and create it if it doesn't + * @param {object} Options - the options object + * @returns {Promise} + */ +async function ensurePluginExists(Options) { + // If pluginName is not provided, we'll let findPluginDirectory handle prompting + if (!Options.pluginName) { + return; + } + + var pluginsDir = path.join(process.cwd(), "developer", "plugins"); + + // Create developer/plugins directory if it doesn't exist + if (!fs.existsSync(pluginsDir)) { + fs.mkdirSync(pluginsDir, { recursive: true }); + } + + var pluginDirectoryName = require(path.join( + __dirname, + "..", + "utils", + "pluginDirectoryName" + )); + + // Try to find plugin directory - could be exact match or ab_plugin_* format + var expectedDirName = pluginDirectoryName(Options.pluginName); + var possibleDirs = [ + Options.pluginName, + expectedDirName, + // Also try lowercase version for backwards compatibility + Options.pluginName.toLowerCase().replace(/[^a-z0-9]/g, "_"), + ]; + var foundDir = null; + var pluginFiles = fs.readdirSync(pluginsDir); + for (var pf in pluginFiles) { + var pluginFilePath = path.join(pluginsDir, pluginFiles[pf]); + if (fs.statSync(pluginFilePath).isDirectory()) { + if ( + pluginFiles[pf] === Options.pluginName || + pluginFiles[pf].toLowerCase() === + Options.pluginName.toLowerCase() || + possibleDirs.indexOf(pluginFiles[pf]) !== -1 + ) { + foundDir = pluginFiles[pf]; + break; + } + } + } + + // If plugin doesn't exist, create it + if (!foundDir) { + console.log( + `... plugin "${Options.pluginName}" not found, creating new plugin...` + ); + + var pluginNew = require(path.join(__dirname, "pluginNew")); + + // Create a new options object for pluginNew + // pluginNew expects Options.name, not Options.pluginName + var newPluginOptions = { + _: [Options.pluginName], // Pass pluginName as the name parameter + }; + + // Copy any other options that might be useful + for (var o in Options) { + if ( + o !== "pluginName" && + o !== "objectName" && + o !== "type" && + o !== "_" + ) { + newPluginOptions[o] = Options[o]; + } + } + + await pluginNew.run(newPluginOptions); + + // After pluginNew runs, the plugin directory should exist + // pluginNew uses the same pluginDirectoryName utility, so expectedDirName should match + var createdPluginDir = path.join(pluginsDir, expectedDirName); + + if (fs.existsSync(createdPluginDir)) { + Options.pluginDir = createdPluginDir; + Options.pluginDirName = expectedDirName; + console.log(`... plugin created successfully`); + } else { + throw new Error( + `Plugin was created but directory not found: ${createdPluginDir}` + ); + } + } +} + +/** + * @function prepareCommandOptions + * Prepare options object for a sub-command with proper _ array values + * @param {object} baseOptions - the base options object with all processed values + * @param {array} argsArray - array of arguments to set in Options._ for the command + * @returns {object} new options object with _ array set properly + */ +function prepareCommandOptions(baseOptions, argsArray) { + // Create a copy of the options + var commandOptions = {}; + for (var o in baseOptions) { + commandOptions[o] = baseOptions[o]; + } + + // Set the _ array with the proper values for the command to shift + commandOptions._ = argsArray.slice(); + + return commandOptions; +} + +module.exports = { + checkDependencies, + findPluginDirectory, + questions, + updatePlatformJs, + updateManifest, + copyTemplateFiles, + ensurePluginExists, + prepareCommandOptions, +}; diff --git a/lib/tasks/pluginPlatformProperties.js b/lib/tasks/pluginPlatformProperties.js new file mode 100644 index 0000000..4748a6a --- /dev/null +++ b/lib/tasks/pluginPlatformProperties.js @@ -0,0 +1,208 @@ +// +// pluginPlatformProperties +// add properties code to an existing plugin +// +// options: +// +// +var fs = require("fs"); +var path = require("path"); +var shell = require("shelljs"); +var utils = require(path.join(__dirname, "..", "utils", "utils")); +var generator = require(path.join(__dirname, "pluginPlatformGenerator")); + +var Options = {}; // the running options for this command. + +// +// Build the PluginPlatformProperties Command +// +var Command = new utils.Resource({ + command: "pluginPlatformProperties", + params: "", + descriptionShort: + "add properties code to an existing plugin (updated architecture).", + descriptionLong: ` +`, +}); + +module.exports = Command; + +Command.help = function () { + console.log(` + + usage: $ appbuilder plugin platform-properties [pluginName] [objectName] [options] + + add properties code to an existing plugin in developer/plugins/ + + Options: + [pluginName] the name of the plugin to add properties to (will prompt if not provided). + [objectName] the object name for the properties (e.g., "ObjectNetsuite" creates FNObjectNetsuite.js). Defaults to pluginName if omitted. + +`); +}; + +Command.run = async function (options) { + // copy our passed in options to our Options + for (var o in options) { + Options[o] = options[o]; + } + + Options.pluginName = Options._.shift(); + Options.objectName = Options._.shift(); + // Properties always has type = "properties" + Options.type = "properties"; + + await generator.checkDependencies(); + await generator.findPluginDirectory(Options, "properties"); + await generator.questions(Options, "properties"); + await copyTemplateFiles(); + await updateWebpackConfig(); + await updateManifest(); +}; + +/** + * @function copyTemplateFiles + * copy our template files into the plugin directory + * @returns {Promise} + */ +function copyTemplateFiles() { + return new Promise((resolve, reject) => { + var propertiesJsPath = path.join(Options.pluginDir, "properties.js"); + var propertiesJsExists = fs.existsSync(propertiesJsPath); + + // Change into the plugin directory + shell.pushd("-q", Options.pluginDir); + + // Use fileCopyTemplates to copy template files + // This will copy properties/FN[objectNamePascal].js and create properties.js if it doesn't exist + utils.fileCopyTemplates( + "pluginPlatformProperties", + Options, + [], + (err) => { + if (err) { + shell.popd("-q"); + reject(err); + return; + } + + // Handle properties.js - update if exists, create if not + if (propertiesJsExists) { + generator + .updatePlatformJs(propertiesJsPath, Options, "properties") + .then(() => { + shell.popd("-q"); + resolve(); + }) + .catch((err) => { + shell.popd("-q"); + reject(err); + }); + } else { + // properties.js was created by fileCopyTemplates, but we need to verify it + if (fs.existsSync(propertiesJsPath)) { + console.log("... created: properties.js"); + } + shell.popd("-q"); + resolve(); + } + } + ); + }); +} + +/** + * @function updateWebpackConfig + * Check webpack.common.js for browserEsm entry with properties.js, ensure it exists + * @returns {Promise} + */ +function updateWebpackConfig() { + return new Promise((resolve, reject) => { + var webpackPath = path.join(Options.pluginDir, "webpack.common.js"); + + if (!fs.existsSync(webpackPath)) { + reject( + new Error( + `webpack.common.js not found in plugin directory: ${Options.pluginDir}` + ) + ); + return; + } + + var contents = fs.readFileSync(webpackPath, "utf8"); + + // Check if browserEsm entry already has properties.js + var hasPropertiesEntry = + /entry:\s*\{[^}]*properties:\s*path\.join\(APP,\s*["']properties\.js["']\)/i.test( + contents + ); + + if (hasPropertiesEntry) { + console.log("... webpack.common.js already has properties.js entry"); + resolve(); + return; + } + + // Check if browserEsm exists + var hasBrowserEsm = /const\s+browserEsm\s*=/i.test(contents); + + if (!hasBrowserEsm) { + console.log( + "... webpack.common.js does not have browserEsm entry. It should be added when creating the plugin." + ); + resolve(); + return; + } + + // Add properties.js to browserEsm entry + // Look for: entry: { web: ..., properties: ... } + var entryPattern = + /(entry:\s*\{[^}]*)(web:\s*path\.join\(APP,\s*["']web\.js["']\))/i; + if (entryPattern.test(contents)) { + // Insert properties entry after web + contents = contents.replace( + entryPattern, + `$1$2,\n properties: path.join(APP, "properties.js")` + ); + fs.writeFileSync(webpackPath, contents); + console.log("... updated webpack.common.js with properties.js entry"); + } else { + // Try to find entry object and add properties + var simpleEntryPattern = /(entry:\s*\{)/i; + if (simpleEntryPattern.test(contents)) { + contents = contents.replace( + simpleEntryPattern, + `$1\n properties: path.join(APP, "properties.js"),` + ); + fs.writeFileSync(webpackPath, contents); + console.log( + "... updated webpack.common.js with properties.js entry" + ); + } + } + + resolve(); + }); +} + +/** + * @function updateManifest + * Update manifest.json to add properties entry to .plugins array + * @returns {Promise} + */ +function updateManifest() { + // Uniqueness check: only one properties entry (platform:web, type:properties) per plugin + var uniquenessCheck = function (plugins) { + return plugins.some( + (p) => p.platform === "web" && p.type === "properties" + ); + }; + + var platformPath = `./${Options.pluginName}_properties.mjs`; + return generator.updateManifest( + Options, + "web", + platformPath, + uniquenessCheck + ); +} diff --git a/lib/tasks/pluginPlatformService.js b/lib/tasks/pluginPlatformService.js new file mode 100644 index 0000000..3e0a823 --- /dev/null +++ b/lib/tasks/pluginPlatformService.js @@ -0,0 +1,235 @@ +// +// pluginPlatformService +// add service code to an existing plugin +// +// options: +// +// +var fs = require("fs"); +var path = require("path"); +var shell = require("shelljs"); +var utils = require(path.join(__dirname, "..", "utils", "utils")); +var fileRender = require(path.join(__dirname, "..", "utils", "fileRender")); +var fileTemplatePath = require(path.join( + __dirname, + "..", + "utils", + "fileTemplatePath" +)); +var generator = require(path.join(__dirname, "pluginPlatformGenerator")); + +var Options = {}; // the running options for this command. + +// +// Build the PluginPlatformService Command +// +var Command = new utils.Resource({ + command: "pluginPlatformService", + params: "", + descriptionShort: + "add service code to an existing plugin (updated architecture).", + descriptionLong: ` +`, +}); + +module.exports = Command; + +Command.help = function () { + console.log(` + + usage: $ appbuilder plugin platform-service [pluginName] [type] [objectName] [options] + + add service code to an existing plugin in developer/plugins/ + + Options: + [pluginName] the name of the plugin to add service to (will prompt if not provided). + [type] the type of the service entry (e.g., "object", "model", etc.). + [objectName] the object name for the service (e.g., "ObjectNetsuite" creates FNObjectNetsuite.js). Defaults to pluginName if omitted. + +`); +}; + +Command.run = async function (options) { + // copy our passed in options to our Options + for (var o in options) { + Options[o] = options[o]; + } + + Options.pluginName = Options._.shift(); + Options.type = Options._.shift(); + Options.objectName = Options._.shift(); + + await generator.checkDependencies(); + await generator.findPluginDirectory(Options, "service"); + await generator.questions(Options, "service"); + await copyTemplateFiles(); + await updateWebpackConfig(); + await updateManifest(); +}; + +/** + * @function copyTemplateFiles + * copy our template files into the plugin directory + * @returns {Promise} + */ +function copyTemplateFiles() { + return new Promise((resolve, reject) => { + var serviceJsPath = path.join(Options.pluginDir, "service.js"); + var serviceJsExists = fs.existsSync(serviceJsPath); + + // Change into the plugin directory + shell.pushd("-q", Options.pluginDir); + + // Use fileCopyTemplates to copy template files (will skip service.js if it exists) + // This will copy service/FN[objectNamePascal].js and create service.js if it doesn't exist + utils.fileCopyTemplates("pluginPlatformService", Options, [], (err) => { + if (err) { + shell.popd("-q"); + reject(err); + return; + } + + // Handle service.js - update if exists, create if not + if (serviceJsExists) { + generator + .updatePlatformJs(serviceJsPath, Options, "service") + .then(() => { + shell.popd("-q"); + resolve(); + }) + .catch((err) => { + shell.popd("-q"); + reject(err); + }); + } else { + // service.js was created by fileCopyTemplates, but we need to verify it + if (fs.existsSync(serviceJsPath)) { + console.log("... created: service.js"); + } + shell.popd("-q"); + resolve(); + } + }); + }); +} + +/** + * @function updateWebpackConfig + * Check webpack.common.js for serviceUmd entry, add if missing + * @returns {Promise} + */ +function updateWebpackConfig() { + return new Promise((resolve, reject) => { + var webpackPath = path.join(Options.pluginDir, "webpack.common.js"); + + if (!fs.existsSync(webpackPath)) { + reject( + new Error( + `webpack.common.js not found in plugin directory: ${Options.pluginDir}` + ) + ); + return; + } + + var contents = fs.readFileSync(webpackPath, "utf8"); + + // Check if serviceUmd is already defined and exported + var hasServiceUmdDef = /const\s+serviceUmd\s*=/i.test(contents); + var hasServiceUmdInExports = /module\.exports.*serviceUmd/i.test( + contents + ); + + if (hasServiceUmdDef && hasServiceUmdInExports) { + console.log("... webpack.common.js already has serviceUmd entry"); + resolve(); + return; + } + + // Read the template for serviceUmd entry + var templatePath = path.join( + fileTemplatePath(), + "_pluginService_serviceUmd.js" + ); + + if (!fs.existsSync(templatePath)) { + reject(new Error(`Template file not found: ${templatePath}`)); + return; + } + + var serviceUmdCode = fileRender(templatePath, Options); + var modified = false; + + // If serviceUmd is not defined, add it before module.exports + if (!hasServiceUmdDef) { + // Find where to insert - look for module.exports = [ + var insertPattern = /(module\.exports\s*=\s*\[)/; + if (insertPattern.test(contents)) { + // Insert serviceUmd definition before module.exports + contents = contents.replace( + insertPattern, + serviceUmdCode + "\n\n$1" + ); + modified = true; + } else { + // If no array export found, try to find just module.exports + var simplePattern = /(module\.exports\s*=)/; + if (simplePattern.test(contents)) { + contents = contents.replace( + simplePattern, + serviceUmdCode + "\n\n$1" + ); + modified = true; + } else { + // Append at the end + contents = contents + "\n\n" + serviceUmdCode; + modified = true; + } + } + } + + // If serviceUmd is defined but not in exports, or if we just added it, add it to the exports array + if (!hasServiceUmdInExports) { + // Find module.exports = [browserEsm] or similar and add serviceUmd + var exportsPattern = /module\.exports\s*=\s*\[([^\]]+)\]/; + if (exportsPattern.test(contents)) { + var match = contents.match(exportsPattern); + var currentExports = match[1].trim(); + // Check if serviceUmd is already in the exports (shouldn't be, but just in case) + if (currentExports.indexOf("serviceUmd") === -1) { + var newExports = currentExports + ", serviceUmd"; + contents = contents.replace( + exportsPattern, + `module.exports = [${newExports}]` + ); + modified = true; + } + } + } + + if (modified) { + fs.writeFileSync(webpackPath, contents); + console.log("... updated webpack.common.js with serviceUmd entry"); + } + resolve(); + }); +} + +/** + * @function updateManifest + * Update manifest.json to add service entry to .plugins array + * @returns {Promise} + */ +function updateManifest() { + // Uniqueness check: only one service entry per plugin + var uniquenessCheck = function (plugins) { + return plugins.some((p) => p.platform === "service"); + }; + + var platformPath = `./${Options.pluginName}_service.js`; + return generator.updateManifest( + Options, + "service", + platformPath, + uniquenessCheck + ); +} diff --git a/lib/tasks/pluginPlatformWeb.js b/lib/tasks/pluginPlatformWeb.js new file mode 100644 index 0000000..f776591 --- /dev/null +++ b/lib/tasks/pluginPlatformWeb.js @@ -0,0 +1,201 @@ +// +// pluginPlatformWeb +// add web code to an existing plugin +// +// options: +// +// +var fs = require("fs"); +var path = require("path"); +var shell = require("shelljs"); +var utils = require(path.join(__dirname, "..", "utils", "utils")); +var generator = require(path.join(__dirname, "pluginPlatformGenerator")); + +var Options = {}; // the running options for this command. + +// +// Build the PluginPlatformWeb Command +// +var Command = new utils.Resource({ + command: "pluginPlatformWeb", + params: "", + descriptionShort: + "add web code to an existing plugin (updated architecture).", + descriptionLong: ` +`, +}); + +module.exports = Command; + +Command.help = function () { + console.log(` + + usage: $ appbuilder plugin platform-web [pluginName] [type] [objectName] [options] + + add web code to an existing plugin in developer/plugins/ + + Options: + [pluginName] the name of the plugin to add web code to (will prompt if not provided). + [type] the type of the web entry (e.g., "object", "model", etc.). + [objectName] the object name for the web (e.g., "ObjectNetsuite" creates FNObjectNetsuite.js). Defaults to pluginName if omitted. + +`); +}; + +Command.run = async function (options) { + // copy our passed in options to our Options + for (var o in options) { + Options[o] = options[o]; + } + + Options.pluginName = Options._.shift(); + Options.type = Options._.shift(); + Options.objectName = Options._.shift(); + + await generator.checkDependencies(); + await generator.findPluginDirectory(Options, "web"); + await generator.questions(Options, "web"); + await copyTemplateFiles(); + await updateWebpackConfig(); + await updateManifest(); +}; + +/** + * @function copyTemplateFiles + * copy our template files into the plugin directory + * @returns {Promise} + */ +function copyTemplateFiles() { + return new Promise((resolve, reject) => { + var webJsPath = path.join(Options.pluginDir, "web.js"); + var webJsExists = fs.existsSync(webJsPath); + + // Change into the plugin directory + shell.pushd("-q", Options.pluginDir); + + // Use fileCopyTemplates to copy template files (will skip web.js if it exists) + // This will copy web/FN[objectNamePascal].js and create web.js if it doesn't exist + utils.fileCopyTemplates("pluginPlatformWeb", Options, [], (err) => { + if (err) { + shell.popd("-q"); + reject(err); + return; + } + + // Handle web.js - update if exists, create if not + if (webJsExists) { + generator + .updatePlatformJs(webJsPath, Options, "web") + .then(() => { + shell.popd("-q"); + resolve(); + }) + .catch((err) => { + shell.popd("-q"); + reject(err); + }); + } else { + // web.js was created by fileCopyTemplates, but we need to verify it + if (fs.existsSync(webJsPath)) { + console.log("... created: web.js"); + } + shell.popd("-q"); + resolve(); + } + }); + }); +} + +/** + * @function updateWebpackConfig + * Check webpack.common.js for browserEsm entry with web.js, ensure it exists + * @returns {Promise} + */ +function updateWebpackConfig() { + return new Promise((resolve, reject) => { + var webpackPath = path.join(Options.pluginDir, "webpack.common.js"); + + if (!fs.existsSync(webpackPath)) { + reject( + new Error( + `webpack.common.js not found in plugin directory: ${Options.pluginDir}` + ) + ); + return; + } + + var contents = fs.readFileSync(webpackPath, "utf8"); + + // Check if browserEsm entry already has web.js + var hasWebEntry = + /entry:\s*\{[^}]*web:\s*path\.join\(APP,\s*["']web\.js["']\)/i.test( + contents + ); + + if (hasWebEntry) { + console.log("... webpack.common.js already has web.js entry"); + resolve(); + return; + } + + // Check if browserEsm exists + var hasBrowserEsm = /const\s+browserEsm\s*=/i.test(contents); + + if (!hasBrowserEsm) { + console.log( + "... webpack.common.js does not have browserEsm entry. It should be added when creating the plugin." + ); + resolve(); + return; + } + + // Add web.js to browserEsm entry + // Look for: entry: { web: ..., properties: ... } + var entryPattern = + /(entry:\s*\{[^}]*)(properties:\s*path\.join\(APP,\s*["']properties\.js["']\))/i; + if (entryPattern.test(contents)) { + // Insert web entry before properties + contents = contents.replace( + entryPattern, + `$1web: path.join(APP, "web.js"),\n $2` + ); + fs.writeFileSync(webpackPath, contents); + console.log("... updated webpack.common.js with web.js entry"); + } else { + // Try to find entry object and add web + var simpleEntryPattern = /(entry:\s*\{)/i; + if (simpleEntryPattern.test(contents)) { + contents = contents.replace( + simpleEntryPattern, + `$1\n web: path.join(APP, "web.js"),` + ); + fs.writeFileSync(webpackPath, contents); + console.log("... updated webpack.common.js with web.js entry"); + } + } + + resolve(); + }); +} + +/** + * @function updateManifest + * Update manifest.json to add web entry to .plugins array + * @returns {Promise} + */ +function updateManifest() { + // Uniqueness check: web entry with same platform and type already exists + // For web platform, there can be 2 entries: one with type != "properties" and one with type == "properties" + var uniquenessCheck = function (plugins) { + var typeValue = Options.type || "object"; + return plugins.some((p) => p.platform === "web" && p.type === typeValue); + }; + + var platformPath = `./${Options.pluginName}_web.mjs`; + return generator.updateManifest( + Options, + "web", + platformPath, + uniquenessCheck + ); +} diff --git a/lib/utils/pluginDirectoryName.js b/lib/utils/pluginDirectoryName.js new file mode 100644 index 0000000..44b7e73 --- /dev/null +++ b/lib/utils/pluginDirectoryName.js @@ -0,0 +1,33 @@ +/** + * @function pluginDirectoryName + * Convert a plugin name to the standard directory name format: ab_plugin_ + * This ensures consistent directory naming across all plugin-related commands. + * + * Examples: + * "Netsuite API" -> "ab_plugin_netsuite_api" + * "NetsuiteAPI" -> "ab_plugin_netsuite_api" + * "My Plugin" -> "ab_plugin_my_plugin" + * + * @param {string} pluginName - The plugin name to convert + * @return {string} The directory name in format ab_plugin_ + */ +module.exports = function (pluginName) { + if (!pluginName || typeof pluginName !== "string") { + return ""; + } + + // Remove spaces + var nameNoSpaces = pluginName.replaceAll(" ", ""); + + // Convert camelCase to snake_case + // Insert underscore before capital letters that follow lowercase or other capitals + var snakeCase = nameNoSpaces + .replace(/([a-z])([A-Z])/g, "$1_$2") // lowercase followed by uppercase + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") // sequence of capitals followed by capital+lowercase + .toLowerCase() // Convert to lowercase + .replace(/[^a-z0-9]/g, "_") // Replace any remaining non-alphanumeric with underscore + .replace(/_+/g, "_") // Replace multiple underscores with single underscore + .replace(/^_|_$/g, ""); // Remove leading/trailing underscores + + return `ab_plugin_${snakeCase}`; +}; diff --git a/templates/_pluginService_serviceUmd.js b/templates/_pluginService_serviceUmd.js new file mode 100644 index 0000000..4fcd2c1 --- /dev/null +++ b/templates/_pluginService_serviceUmd.js @@ -0,0 +1,14 @@ +// 2) Service UMD bundle +const serviceUmd = { + ...common, + name: "serviceUmd", + entry: { service: path.join(APP, "service.js") }, + output: { + filename: "AB<%= pluginName %>_service.js", + library: { name: "Plugin", type: "umd" }, + globalObject: "this", + // keep default iife=true implicitly (no warning) + }, + target: "node", // or 'node' if server-side +}; + diff --git a/templates/plugin/.eslintrc.js b/templates/plugin/.eslintrc.js new file mode 100644 index 0000000..c211803 --- /dev/null +++ b/templates/plugin/.eslintrc.js @@ -0,0 +1,55 @@ +// ╔═╗╔═╗╦ ╦╔╗╔╔╦╗┬─┐┌─┐ +// ║╣ ╚═╗║ ║║║║ ║ ├┬┘│ +// o╚═╝╚═╝╩═╝╩╝╚╝ ╩ ┴└─└─┘ +// A set of basic code conventions designed to encourage quality and consistency +// across your app's code base. These rules are checked against automatically +// any time you run `npm test`. +// +// > Note: If you're using mocha, you'll want to add an extra override file to your +// > `test/` folder so that eslint will tolerate mocha-specific globals like `before` +// > and `describe`. +// Designed for ESLint v4. +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +module.exports = { + env: { + node: true, + es6: true, + }, + + globals: { + // io: true, + $$: true, + webix: true, + window: true, + }, + + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, + + root: true, + + // extending recommended config and config derived from eslint-config-prettier + extends: ["eslint:recommended", "prettier"], + + // activating eslint-plugin-prettier (--fix stuff) + plugins: ["prettier"], + + rules: { + // customizing prettier rules (unfortunately not many of them are customizable) + "prettier/prettier": [ + "error", + { + arrowParens: "always", + endOfLine: "lf", + printWidth: 80, + tabWidth: 3, + }, + ], + + // eslint rule customization here: + "no-console": 0, // allow console.log() in our services + }, +}; + diff --git a/templates/plugin/.gitignore b/templates/plugin/.gitignore new file mode 100644 index 0000000..a31451f --- /dev/null +++ b/templates/plugin/.gitignore @@ -0,0 +1,26 @@ + +// node modules +node_modules + +// temporary files +.tmp +leftOffHere.txt + +// misc +*.map +*~ +*# +.DS_STORE +.netbeans +nbproject +.idea +.node_history +dump.rdb + +npm-debug.log +lib-cov +*.seed +*.log +*.out +*.pid + diff --git a/templates/plugin/README.md b/templates/plugin/README.md new file mode 100644 index 0000000..765ff05 --- /dev/null +++ b/templates/plugin/README.md @@ -0,0 +1,4 @@ +# ab_plugin_<%= pluginName.toLowerCase() %> + +(AppBuilder) <%= description || 'A new AppBuilder plugin' %> + diff --git a/templates/plugin/index.js b/templates/plugin/index.js new file mode 100644 index 0000000..ddb1d44 --- /dev/null +++ b/templates/plugin/index.js @@ -0,0 +1,16 @@ +// NOTE: our webpack build process will ignore this file and +// create service and web compiled files instead. +// Service and web directories will be added as optional commands later +export default function register(API) { + // TODO: Add service and web registration when those directories are created + switch (API.platform) { + case "service": + // service(API); + break; + + case "web": + // web(API); + break; + } +} + diff --git a/templates/plugin/manifest.json b/templates/plugin/manifest.json new file mode 100644 index 0000000..e005f77 --- /dev/null +++ b/templates/plugin/manifest.json @@ -0,0 +1,8 @@ +{ + "name": "<%= pluginNameDisplay || pluginName %>", + "description": "<%= description || 'A new AppBuilder plugin' %>", + "key": "<%= pluginKey || pluginName.toLowerCase().replace(/[^a-z0-9]/g, '_') %>", + "version": "0.0.0", + "icon": "<%= icon || 'fa-puzzle-piece' %>", + "plugins": [] +} diff --git a/templates/plugin/package.json b/templates/plugin/package.json new file mode 100644 index 0000000..4492136 --- /dev/null +++ b/templates/plugin/package.json @@ -0,0 +1,38 @@ +{ + "name": "ab-plugin-<%= pluginName.toLowerCase() %>", + "version": "1.0.0", + "description": "<%= description || 'A new AppBuilder plugin' %>", + "main": "index.js", + "scripts": { + "lint": "eslint . styles/team-widget.css --fix --max-warnings=0 --report-unused-disable-directives && echo '✔ Your .js files look good.'", + "test": "echo \"Error: no test specified\" && exit 1", + "build:update": "webpack-cli --config webpack.prod.js", + "build:dev": "webpack-cli --config webpack.dev.js", + "watch": "webpack-cli --config webpack.dev.js --watch --progress" + }, + "author": "<%= author || 'AppBuilder Developer' %>", + "license": "MIT", + "devDependencies": { + "@babel/preset-env": "^7.27.2", + "babel-loader": "^10.0.0", + "compression-webpack-plugin": "^11.1.0", + "eslint": "^8.21.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.2.1", + "prettier": "^2.2.1", + "style-loader": "^4.0.0", + "webpack": "^5.102.1", + "webpack-cli": "^6.0.1", + "webpack-merge": "^6.0.1" + }, + "dependencies": { + "axios": "^1.10.0", + "buffer": "^6.0.3", + "crypto-browserify": "^3.12.1", + "oauth-1.0a": "^2.2.6", + "process": "^0.11.10", + "stream-browserify": "^3.0.0", + "vm-browserify": "^1.1.2" + } +} + diff --git a/templates/plugin/webpack.common.js b/templates/plugin/webpack.common.js new file mode 100644 index 0000000..58036b2 --- /dev/null +++ b/templates/plugin/webpack.common.js @@ -0,0 +1,91 @@ +// developer/plugins/[pluginName]/webpack.common.js +const path = require("path"); +const APP = path.resolve(__dirname); +const webpack = require("webpack"); + +const common = { + context: APP, + module: { + rules: [ + { test: /\.css$/, use: ["style-loader"] }, + { test: /\.css$/, loader: "css-loader", options: { url: false } }, + { + test: /\.(eot|woff|woff2|svg|ttf)([?]?.*)$/, + use: ["url-loader?limit=10000000"], + }, + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + presets: [["@babel/preset-env", { modules: false }]], + }, + }, + }, + ], + }, + plugins: [ + new webpack.DefinePlugin({ + WEBPACK_MODE: JSON.stringify("production"), + VERSION: JSON.stringify(process.env.npm_package_version), + }), + new webpack.ProvidePlugin({ Buffer: ["buffer", "Buffer"] }), + ], + resolve: { + alias: { + assets: path.resolve(__dirname, "..", "..", "..", "assets"), + "axios-http": path.resolve( + __dirname, + "node_modules/axios/lib/adapters/http.js" + ), + }, + fallback: { + process: false, + vm: require.resolve("vm-browserify"), + crypto: require.resolve("crypto-browserify"), + stream: require.resolve("stream-browserify"), + buffer: require.resolve("buffer/"), + }, + }, +}; + +// 1) Browser ESM bundles +const browserEsm = { + ...common, + name: "browserEsm", + entry: { + web: path.join(APP, "web.js"), + properties: path.join(APP, "properties.js"), + }, + output: { + filename: "AB<%= pluginName %>_[name].mjs", + module: true, + iife: false, + chunkFormat: "module", + library: { type: "module" }, + }, + experiments: { + outputModule: true, // allow output.module + }, + target: ["web", "es2020"], + // Optional: make sure webpack doesn't try to split/runtime-chunk into non-ESM: + optimization: { runtimeChunk: false, splitChunks: false }, +}; + +// 2) Service UMD bundle +const serviceUmd = { + ...common, + name: "serviceUmd", + entry: { service: path.join(APP, "service.js") }, + output: { + filename: "AB<%= pluginName %>_service.js", + library: { name: "Plugin", type: "umd" }, + globalObject: "this", + // keep default iife=true implicitly (no warning) + }, + target: "node", // or 'node' if server-side +}; + +module.exports = [browserEsm, serviceUmd]; + diff --git a/templates/plugin/webpack.dev.js b/templates/plugin/webpack.dev.js new file mode 100644 index 0000000..ed9b29d --- /dev/null +++ b/templates/plugin/webpack.dev.js @@ -0,0 +1,33 @@ +const path = require("path"); +const APP = path.resolve(__dirname); +const { merge } = require("webpack-merge"); +const commons = require("./webpack.common.js"); +// exports [browserEsm, serviceUmd] + +// const outPath = path.join( +// APP, +// "..", +// "..", +// "web", +// "assets", +// "ab_plugins", +// "ab-<%= pluginName.toLowerCase() %>" +// ); +const outPath = path.join(APP, "dev"); + +let configs = null; +let myChanges = { + output: { + path: outPath, + }, + mode: "development", + devtool: "source-map", +}; + +if (Array.isArray(commons)) { + configs = commons.map((cfg) => merge(cfg, myChanges)); +} else { + configs = merge(commons, myChanges); +} +module.exports = configs; + diff --git a/templates/plugin/webpack.prod.js b/templates/plugin/webpack.prod.js new file mode 100644 index 0000000..f874081 --- /dev/null +++ b/templates/plugin/webpack.prod.js @@ -0,0 +1,32 @@ +const path = require("path"); +const APP = path.resolve(__dirname); +const { merge } = require("webpack-merge"); +const commons = require("./webpack.common.js"); // exports [browserEsm, serviceUmd] + +// todo: move this to a /dist folder in the plugin root +const outPath = path.join( + APP, + "..", + "..", + "web", + "assets", + "ab_plugins", + "ab-<%= pluginName.toLowerCase() %>" +); + +module.exports = commons.map((cfg) => + merge(cfg, { + output: { + path: outPath, + // filenames come from each per-entry config in webpack.common.js + }, + mode: "production", + devtool: false, + optimization: { + minimize: true, + }, + performance: { + hints: false, + }, + }) +); diff --git a/templates/pluginObject/properties/FN[objectNamePascal].js b/templates/pluginObject/properties/FN[objectNamePascal].js new file mode 100644 index 0000000..fe0943c --- /dev/null +++ b/templates/pluginObject/properties/FN[objectNamePascal].js @@ -0,0 +1,114 @@ +// <%= fnObjectName %> Properties +// A properties side import for an ABObject. +// + +export default function <%= fnObjectName %>({ + ABPropertiesObjectPlugin, + // ABUIPlugin, +}) { + return class AB<%= objectNamePascal %> extends ABPropertiesObjectPlugin { + constructor(...params) { + super(...params); + + // let myBase = AB<%= objectNamePascal %>.getPluginKey(); + // this.UI_Credentials = FNCredentials(this.AB, myBase, ABUIPlugin); + } + + static getPluginKey() { + return "<%= pluginKey %>"; + } + + static getPluginType() { + return "properties-object"; + } + + header() { + // this is the name used when choosing the Object Type + // tab selector. + let L = this.L(); + return L("<%= objectName %>"); + } + + rules() { + return { + // name: webix.rules.isNotEmpty, + }; + } + + elements() { + let L = this.L(); + // return the webix form element definitions to appear on the page. + return [ + // { + // rows: [ + // { + // view: "text", + // label: L("Name"), + // name: "name", + // required: true, + // placeholder: L("Object name"), + // labelWidth: 70, + // }, + // { + // view: "checkbox", + // label: L("Read Only"), + // name: "readonly", + // value: 0, + // // disabled: true, + // }, + // ], + // }, + ]; + } + + async init(AB) { + this.AB = AB; + + this.$form = $$(this.ids.form); + AB.Webix.extend(this.$form, webix.ProgressBar); + + // await this.UI_Credentials.init(AB); + + // this.UI_Credentials.show(); + + // // "verified" is triggered on the credentials tab once we verify + // // the entered data is successful. + // this.UI_Credentials.on("verified", () => { + // + // }); + } + + formClear() { + this.$form.clearValidation(); + this.$form.clear(); + + // this.UI_Credentials.formClear(); + + $$(this.ids.buttonSave).disable(); + } + + async formIsValid() { + var Form = $$(this.ids.form); + + Form?.clearValidation(); + + // if it doesn't pass the basic form validation, return: + if (!Form.validate()) { + $$(this.ids.buttonSave)?.enable(); + return false; + } + return true; + } + + async formValues() { + // let L = this.L(); + + var Form = $$(this.ids.form); + let values = Form.getValues(); + + // values.credentials = this.UI_Credentials.getValues(); + + return values; + } + }; +} \ No newline at end of file diff --git a/templates/pluginObject/service/FN[objectNamePascal].js b/templates/pluginObject/service/FN[objectNamePascal].js new file mode 100644 index 0000000..bfb2c3d --- /dev/null +++ b/templates/pluginObject/service/FN[objectNamePascal].js @@ -0,0 +1,42 @@ +// <%= fnObjectName %> Service +// A service side import for an ABObject. +// +import <%= fnObjectName %>Model from "./FN<%= objectNamePascal %>Model.js"; + +export default function <%= fnObjectName %>({ + /*AB,*/ + ABObjectPlugin, + ABModelPlugin, +}) { + + const ABModel<%= objectNamePascal %> = <%= fnObjectName %>Model({ ABModelPlugin }); + + return class AB<%= objectNamePascal %> extends ABObjectPlugin { + constructor(...params) { + super(...params); + } + + static getPluginKey() { + return "<%= pluginKey %>"; + } + + static getPluginType() { + return "object"; + } + + /** + * @method model + * return a Model object that will allow you to interact with the data for + * this ABObjectQuery. + */ + model() { + var model = new ABModel<%= objectNamePascal %>(this); + + // default the context of this model's operations to this object + model.contextKey(this.constructor.contextKey()); + model.contextValues({ id: this.id }); // the datacollection.id + + return model; + } + }; +} \ No newline at end of file diff --git a/templates/pluginObject/service/FN[objectNamePascal]Model.js b/templates/pluginObject/service/FN[objectNamePascal]Model.js new file mode 100644 index 0000000..24137b3 --- /dev/null +++ b/templates/pluginObject/service/FN[objectNamePascal]Model.js @@ -0,0 +1,94 @@ +export default function <%= fnObjectName %>Model({ ABModelPlugin }) { + + return class ABModel<%= objectNamePascal %> extends ABModelPlugin { + constructor(object) { + super(object); + } + + /// + /// Instance Methods + /// + + /** + * @method create + * performs an update operation + * @param {obj} values + * A hash of the new values for this entry. + * @param {Knex.Transaction?} trx - [optional] + * @param {ABUtil.reqService} req + * The request object associated with the current tenant/request + * @return {Promise} resolved with the result of the find() + */ + // eslint-disable-next-line no-unused-vars + // async create(values, trx = null, condDefaults = null, req = null) { + // } + + /** + * @method delete + * performs a delete operation + * @param {string} id + * the primary key for this update operation. + * @param {Knex.Transaction?} trx - [optional] + * + * @return {Promise} resolved with {int} numRows : the # rows affected + */ + // eslint-disable-next-line no-unused-vars + // async delete(id, trx = null, req = null) { + // } + + /** + * @method findAll + * performs a data find with the provided condition. + * @param {obj} cond + * A set of optional conditions to add to the find(): + * @param {obj} conditionDefaults + * A hash of default condition values. + * conditionDefaults.languageCode {string} the default language of + * the multilingual data to return. + * conditionDefaults.username {string} the username of the user + * we should reference on any user based condition + * @param {ABUtil.reqService} req + * The request object associated with the current tenant/request + * @return {Promise} resolved with the result of the find() + */ + // async findAll(cond, conditionDefaults, req) { + // } + + /** + * @method findCount + * performs a data find to get the total Count of a given condition. + * @param {obj} cond + * A set of optional conditions to add to the find(): + * @param {obj} conditionDefaults + * A hash of default condition values. + * conditionDefaults.languageCode {string} the default language of + * the multilingual data to return. + * conditionDefaults.username {string} the username of the user + * we should reference on any user based condition + * @param {ABUtil.reqService} req + * The request object associated with the current tenant/request + * @return {Promise} resolved with the result of the find() + */ + // async findCount(cond, conditionDefaults, req) { + // } + + /** + * @method update + * performs an update operation + * @param {string} id + * the primary key for this update operation. + * @param {obj} values + * A hash of the new values for this entry. + * @param {Knex.Transaction?} trx - [optional] + * + * @return {Promise} resolved with the result of the find() + */ + // eslint-disable-next-line no-unused-vars + // async update(id, values, userData, trx = null, req = null) { + // } + + normalizeData(data) { + super.normalizeData(data); + } + }; +} diff --git a/templates/pluginObject/web/FN[objectNamePascal].js b/templates/pluginObject/web/FN[objectNamePascal].js new file mode 100644 index 0000000..c4e0bb7 --- /dev/null +++ b/templates/pluginObject/web/FN[objectNamePascal].js @@ -0,0 +1,69 @@ +// <%= fnObjectName %> Web +// A web side import for an ABObject. +// +export default function <%= fnObjectName %>({ + /*AB,*/ + ABObjectPlugin, + ABModelPlugin, +}) { + // + // Our ABModel for interacting with the website + // + class ABModel<%= objectNamePascal %> extends ABModelPlugin { + /** + * @method normalizeData() + * For a Netsuite object, there are additional steps we need to handle + * to normalize our data. + */ + normalizeData(data) { + super.normalizeData(data); + } + } + + /// + /// We return the ABObject here + /// + return class AB<%= objectNamePascal %> extends ABObjectPlugin { + // constructor(...params) { + // super(...params); + // } + + static getPluginKey() { + return "<%= pluginKey %>"; + } + + static getPluginType() { + return "object"; + } + + /** + * @method toObj() + * + * properly compile the current state of this ABObjectQuery instance + * into the values needed for saving to the DB. + * + * @return {json} + */ + toObj() { + const result = super.toObj(); + result.plugin_key = this.constructor.getPluginKey(); + + return result; + } + + /** + * @method model + * return a Model object that will allow you to interact with the data for + * this ABObjectQuery. + */ + model() { + var model = new ABModel<%= objectNamePascal %>(this); + + // default the context of this model's operations to this object + model.contextKey(this.constructor.contextKey()); + model.contextValues({ id: this.id }); // the datacollection.id + + return model; + } + }; +} // end of FNObjectNetsuiteAPI diff --git a/templates/pluginPlatformProperties/properties.js b/templates/pluginPlatformProperties/properties.js new file mode 100644 index 0000000..827bbe8 --- /dev/null +++ b/templates/pluginPlatformProperties/properties.js @@ -0,0 +1,8 @@ +import <%= fnObjectName %> from "./properties/<%= fnObjectName %>.js"; + +export default function registerProperties(PluginAPI) { + return [ + <%= fnObjectName %>(PluginAPI) + ]; +} + diff --git a/templates/pluginPlatformService/service.js b/templates/pluginPlatformService/service.js new file mode 100644 index 0000000..15e74b2 --- /dev/null +++ b/templates/pluginPlatformService/service.js @@ -0,0 +1,8 @@ +import <%= fnObjectName %> from "./service/<%= fnObjectName %>.js"; + +export default function registerService(PluginAPI) { + return [ + <%= fnObjectName %>(PluginAPI) + ]; +} + diff --git a/templates/pluginPlatformWeb/web.js b/templates/pluginPlatformWeb/web.js new file mode 100644 index 0000000..a9a9158 --- /dev/null +++ b/templates/pluginPlatformWeb/web.js @@ -0,0 +1,8 @@ +import <%= fnObjectName %> from "./web/<%= fnObjectName %>.js"; + +export default function registerWeb(PluginAPI) { + return [ + <%= fnObjectName %>(PluginAPI) + ]; +} + From cbb2641feda4c9b9a6b6d7ee885d77f9f3f3efd8 Mon Sep 17 00:00:00 2001 From: Johnny Hausman Date: Tue, 11 Nov 2025 15:22:08 +0700 Subject: [PATCH 2/4] [wip] eslint changes --- lib/install.js | 10 ++++++---- lib/oldPlugin.js | 1 - templates/_pluginService_serviceUmd.js | 1 - templates/plugin/.eslintrc.js | 1 - templates/plugin/index.js | 1 - templates/plugin/webpack.common.js | 1 - templates/plugin/webpack.dev.js | 1 - 7 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/install.js b/lib/install.js index 02e6e38..83f1f28 100644 --- a/lib/install.js +++ b/lib/install.js @@ -295,9 +295,11 @@ function removeTempDBInitFiles(done) { */ async function installDeveloperFiles() { if (!Options.develop) return; - if (!shell.which("tar")) { - console.error("Warning! 'tar' is not installed. If install fails, please install it before retrying."); - } + if (!shell.which("tar")) { + console.error( + "Warning! 'tar' is not installed. If install fails, please install it before retrying." + ); + } console.log("... installing developer files (this will take awhile)"); console.log(" ... download digiserve/ab-code-developer"); @@ -315,7 +317,7 @@ async function installDeveloperFiles() { bar = utils.logFormatter.progressBar(" "); bar.start(100, 0, { filename: "/developer" }); } - const checkpoint = 46130; + // const checkpoint = 46130; /** * checpoint for tar command, calulation: * `du -sk --apparent-size developer.tar.bz2 | cut -f 1` / 45 diff --git a/lib/oldPlugin.js b/lib/oldPlugin.js index eb33944..055782a 100644 --- a/lib/oldPlugin.js +++ b/lib/oldPlugin.js @@ -112,4 +112,3 @@ function chooseTask(done) { task.run(Options).then(done).catch(done); } - diff --git a/templates/_pluginService_serviceUmd.js b/templates/_pluginService_serviceUmd.js index 4fcd2c1..c6bc7b6 100644 --- a/templates/_pluginService_serviceUmd.js +++ b/templates/_pluginService_serviceUmd.js @@ -11,4 +11,3 @@ const serviceUmd = { }, target: "node", // or 'node' if server-side }; - diff --git a/templates/plugin/.eslintrc.js b/templates/plugin/.eslintrc.js index c211803..f969733 100644 --- a/templates/plugin/.eslintrc.js +++ b/templates/plugin/.eslintrc.js @@ -52,4 +52,3 @@ module.exports = { "no-console": 0, // allow console.log() in our services }, }; - diff --git a/templates/plugin/index.js b/templates/plugin/index.js index ddb1d44..6e0ddbe 100644 --- a/templates/plugin/index.js +++ b/templates/plugin/index.js @@ -13,4 +13,3 @@ export default function register(API) { break; } } - diff --git a/templates/plugin/webpack.common.js b/templates/plugin/webpack.common.js index 58036b2..3605116 100644 --- a/templates/plugin/webpack.common.js +++ b/templates/plugin/webpack.common.js @@ -88,4 +88,3 @@ const serviceUmd = { }; module.exports = [browserEsm, serviceUmd]; - diff --git a/templates/plugin/webpack.dev.js b/templates/plugin/webpack.dev.js index ed9b29d..7a0fc64 100644 --- a/templates/plugin/webpack.dev.js +++ b/templates/plugin/webpack.dev.js @@ -30,4 +30,3 @@ if (Array.isArray(commons)) { configs = merge(commons, myChanges); } module.exports = configs; - From fca36d53a58d1ee33649507c483b8490ccdb176d Mon Sep 17 00:00:00 2001 From: Johnny Hausman Date: Tue, 11 Nov 2025 16:24:03 +0700 Subject: [PATCH 3/4] [wip] add the oldPlugin tasks --- lib/tasks/oldPluginNew.js | 171 ++++++++++++++++++++++++++++++ lib/tasks/oldPluginPage.js | 210 +++++++++++++++++++++++++++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 lib/tasks/oldPluginNew.js create mode 100644 lib/tasks/oldPluginPage.js diff --git a/lib/tasks/oldPluginNew.js b/lib/tasks/oldPluginNew.js new file mode 100644 index 0000000..0467339 --- /dev/null +++ b/lib/tasks/oldPluginNew.js @@ -0,0 +1,171 @@ +// +// oldPluginNew +// create a new plugin in the developer/plugins directory (old architecture). +// +// options: +// +// +var async = require("async"); +// var fs = require("fs"); +var inquirer = require("inquirer"); +var path = require("path"); +var shell = require("shelljs"); +var utils = require(path.join(__dirname, "..", "utils", "utils")); + +var Options = {}; // the running options for this command. + +// +// Build the Install Command +// +var Command = new utils.Resource({ + command: "oldPluginNew", + params: "", + descriptionShort: + "create a new plugin in developer/plugins/ directory (old architecture).", + descriptionLong: ` +`, +}); + +module.exports = Command; + +Command.help = function () { + console.log(` + + usage: $ appbuilder oldPlugin new [name] [options] + + create a new plugin in [root]/developer/plugins/[name] + + Options: + [name] the name of the service to create. + -d the name of the plugins directory (default: plugins) + +`); +}; + +Command.run = function (options) { + return new Promise((resolve, reject) => { + async.series( + [ + // copy our passed in options to our Options + (done) => { + for (var o in options) { + Options[o] = options[o]; + } + + Options.name = Options._.shift(); + if (!Options.name) { + console.log("missing parameter [name]"); + Command.help(); + process.exit(1); + } + done(); + }, + checkDependencies, + questions, + copyTemplateFiles, + installGitDependencies, + ], + (err) => { + if (err) { + reject(err); + return; + } + resolve(); + } + ); + }); +}; + +/** + * @function checkDependencies + * verify the system has any required dependencies for generating ssl certs. + * @param {function} done node style callback(err) + */ +function checkDependencies(done) { + utils.checkDependencies(["git"], done); +} + +/** + * @function copyTemplateFiles + * copy our template files into the project + * @param {cb(err)} done + */ +function copyTemplateFiles(done) { + var parts = Options.name.split("_"); + for (var p = 0; p < parts.length; p++) { + parts[p] = parts[p].charAt(0).toUpperCase() + parts[p].slice(1); + } + // Options.className = parts.join(""); + Options.pluginName = Options.name.replaceAll(" ", ""); + Options.pluginID = Options.pluginName; + // options.name, no spaces, + Options.description = Options.description || "a new plugin"; // package.json .description + Options.icon = Options.icon || "puzzle-piece"; + Options.fileName = `${Options.pluginID}.js`; + + utils.fileCopyTemplates("oldPluginNew", Options, [], done); +} + +/** + * @function installGitDependencies + * install our initial git dependencies. + * @param {cb(err)} done + */ +function installGitDependencies(done) { + shell.pushd( + "-q", + path.join(process.cwd(), "developer", "plugins", Options.pluginName) + ); + + // init git repo + shell.exec(`git init`); + + console.log("... npm install (this takes a while)"); + shell.exec("npm install"); + // utils.execCli("npm install"); + + shell.popd(); + done(); +} + +/** + * @function questions + * Present the user with a list of configuration questions. + * If the answer for a question is already present in Options, then we + * skip that question. + * @param {cb(err)} done + */ +function questions(done) { + inquirer + .prompt([ + { + name: "description", + type: "input", + message: "Describe this plugin :", + default: "A cool new plugin.", + when: (values) => { + return ( + !values.description && + typeof Options.description == "undefined" + ); + }, + }, + { + name: "icon", + type: "input", + message: "Enter the fontawesome icon reference fa-* :", + default: "puzzle-piece", + when: (values) => { + return !values.icon && typeof Options.icon == "undefined"; + }, + }, + ]) + .then((answers) => { + for (var a in answers) { + Options[a] = answers[a]; + } + // console.log("Options:", Options); + done(); + }) + .catch(done); +} diff --git a/lib/tasks/oldPluginPage.js b/lib/tasks/oldPluginPage.js new file mode 100644 index 0000000..e348577 --- /dev/null +++ b/lib/tasks/oldPluginPage.js @@ -0,0 +1,210 @@ +// +// oldPluginPage +// create a new plugin in the developer/plugins directory (old architecture). +// +// options: +// +// +var async = require("async"); +// var fs = require("fs"); +// var inquirer = require("inquirer"); +var path = require("path"); +// var shell = require("shelljs"); +var utils = require(path.join(__dirname, "..", "utils", "utils")); + +var Options = {}; // the running options for this command. + +// +// Build the Install Command +// +var Command = new utils.Resource({ + command: "oldPluginPage", + params: "", + descriptionShort: + "create a new plugin page in developer/plugins/[plugin] directory (old architecture).", + descriptionLong: ` +`, +}); + +module.exports = Command; + +Command.help = function () { + console.log(` + + usage: $ appbuilder oldPlugin page [plugin] [pageName] + + create a new plugin page in [root]/developer/plugins/[name]/src/rootPages/ + + Options: + [plugin] the name of the plugin we are adding a page to + [pageName] the name of the root page we are creating + +`); +}; + +Command.run = function (options) { + return new Promise((resolve, reject) => { + async.series( + [ + // copy our passed in options to our Options + (done) => { + for (var o in options) { + Options[o] = options[o]; + } + + Options.pluginName = Options._.shift(); + if (!Options.pluginName) { + console.log("missing parameter [plugin]"); + Command.help(); + process.exit(1); + } + Options.pageName = Options._.shift(); + if (!Options.pageName) { + console.log("missing parameter [pageName]"); + Command.help(); + process.exit(1); + } + done(); + }, + checkDependencies, + // questions, + copyTemplateFiles, + updateApplication, + ], + (err) => { + if (err) { + reject(err); + return; + } + resolve(); + } + ); + }); +}; + +/** + * @function checkDependencies + * verify the system has any required dependencies for generating ssl certs. + * @param {function} done node style callback(err) + */ +function checkDependencies(done) { + utils.checkDependencies([], done); +} + +/** + * @function copyTemplateFiles + * copy our template files into the project + * @param {cb(err)} done + */ +function copyTemplateFiles(done) { + // Options.pluginName; + // Options.pageName; + + utils.fileCopyTemplates("oldPluginPage", Options, [], done); +} + +/** + * @function updateApplication() + * Insert this Page info into the plugin/src/application.js file + */ +function updateApplication(done) { + var pathFile = path.join( + "developer", + "plugins", + Options.pluginName, + "src", + "application.js" + ); + + var pageName = Options.pageName.replaceAll(" ", "_"); + // {string} Name of our Page that is OK as a variable (no spaces) + + var tagExport = "export default function (AB) {"; + + var replaceImport = `import ${pageName}Factory from "./rootPages/${Options.pageName}/ui.js"; +${tagExport}`; + + var replaceVariable = `${tagExport} + var ${pageName} = ${pageName}Factory(AB);`; + + var tagAppLink = "return application;"; + var replaceAppLink = `${pageName}.application = application; + ${tagAppLink}`; + + utils.filePatch([ + { + // insert the Factory Import statement + file: pathFile, + tag: tagExport, + replace: replaceImport, + }, + { + // insert the Page Variable from Factory statement + file: pathFile, + tag: tagExport, + replace: replaceVariable, + }, + { + // insert our Page Variable into our _pages + file: pathFile, + tag: "_pages: [", + replace: `_pages: [ ${pageName},`, + }, + { + // Fix any improper page insert (on first insert) + file: pathFile, + tag: ",],", + replace: "],", + }, + { + // Each New Page needs to link back to our application + file: pathFile, + tag: tagAppLink, + replace: replaceAppLink, + }, + ]); + + done(); +} + +/** + * @function questions + * Present the user with a list of configuration questions. + * If the answer for a question is already present in Options, then we + * skip that question. + * @param {cb(err)} done + */ +// function questions(done) { +// inquirer +// .prompt([ +// { +// name: "description", +// type: "input", +// message: "Describe this plugin :", +// default: "A cool new plugin.", +// when: (values) => { +// return ( +// !values.description && +// typeof Options.description == "undefined" +// ); +// }, +// }, +// { +// name: "icon", +// type: "input", +// message: "Enter the fontawesome icon reference fa-* :", +// default: "puzzle-piece", +// when: (values) => { +// return !values.icon && typeof Options.icon == "undefined"; +// }, +// }, +// ]) +// .then((answers) => { +// for (var a in answers) { +// Options[a] = answers[a]; +// } +// // console.log("Options:", Options); +// done(); +// }) +// .catch(done); +// } From 42bbcb2a07096c9fb9f971d54f2fd951f7c9df08 Mon Sep 17 00:00:00 2001 From: Johnny Hausman Date: Wed, 12 Nov 2025 15:29:03 +0700 Subject: [PATCH 4/4] [wip] added new plugin view command --- README.md | 14 ++ lib/plugin.js | 11 ++ lib/tasks/pluginPlatformProperties.js | 2 +- lib/tasks/pluginPlatformService.js | 2 +- lib/tasks/pluginPlatformWeb.js | 2 +- lib/tasks/pluginView.js | 123 +++++++++++++++ .../service/FN[objectNamePascal].js | 4 - .../pluginObject/web/FN[objectNamePascal].js | 4 - .../properties/FN[objectNamePascal].js | 88 +++++++++++ .../properties/FN[objectNamePascal]Editor.js | 98 ++++++++++++ .../pluginView/web/FN[objectNamePascal].js | 145 ++++++++++++++++++ .../web/FN[objectNamePascal]Component.js | 46 ++++++ 12 files changed, 528 insertions(+), 11 deletions(-) create mode 100644 lib/tasks/pluginView.js create mode 100644 templates/pluginView/properties/FN[objectNamePascal].js create mode 100644 templates/pluginView/properties/FN[objectNamePascal]Editor.js create mode 100644 templates/pluginView/web/FN[objectNamePascal].js create mode 100644 templates/pluginView/web/FN[objectNamePascal]Component.js diff --git a/README.md b/README.md index ca105a7..86b8540 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,20 @@ A command line helper for our AppBuilder development. $ sudo npm install -g CruGlobal/ab-cli ``` +# Updating + +To update to the latest version: + +```sh +$ sudo npm update -g CruGlobal/ab-cli +``` + +Or reinstall to ensure you have the latest: + +```sh +$ sudo npm install -g CruGlobal/ab-cli@latest +``` + # Usage ## Development Install (local machine) diff --git a/lib/plugin.js b/lib/plugin.js index 8478653..a48a27d 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -7,6 +7,7 @@ var utils = require(path.join(__dirname, "utils", "utils")); var pluginNew = require(path.join(__dirname, "tasks", "pluginNew.js")); var pluginObject = require(path.join(__dirname, "tasks", "pluginObject.js")); +var pluginView = require(path.join(__dirname, "tasks", "pluginView.js")); var Options = {}; // the running options for this command. @@ -31,6 +32,7 @@ Command.help = function () { [operation]s : new : $ appbuilder plugin new [name] object : $ appbuilder plugin object [pluginName] [objectName] + view : $ appbuilder plugin view [pluginName] [viewName] [options] : name: the name of the plugin @@ -46,6 +48,12 @@ Command.help = function () { - adds object code, objectName defaults to pluginName $ appbuilder plugin object - prompts to select plugin and object name + $ appbuilder plugin view MyPlugin MyWidget + - adds view code (properties and web) to existing plugin with view name + $ appbuilder plugin view MyPlugin + - adds view code, viewName defaults to pluginName + $ appbuilder plugin view + - prompts to select plugin and view name `); }; @@ -97,6 +105,9 @@ async function chooseTask() { case "object": task = pluginObject; break; + case "view": + task = pluginView; + break; } if (!task) { Command.help(); diff --git a/lib/tasks/pluginPlatformProperties.js b/lib/tasks/pluginPlatformProperties.js index 4748a6a..baf9ca1 100644 --- a/lib/tasks/pluginPlatformProperties.js +++ b/lib/tasks/pluginPlatformProperties.js @@ -198,7 +198,7 @@ function updateManifest() { ); }; - var platformPath = `./${Options.pluginName}_properties.mjs`; + var platformPath = `./AB${Options.pluginName}_properties.mjs`; return generator.updateManifest( Options, "web", diff --git a/lib/tasks/pluginPlatformService.js b/lib/tasks/pluginPlatformService.js index 3e0a823..a97240a 100644 --- a/lib/tasks/pluginPlatformService.js +++ b/lib/tasks/pluginPlatformService.js @@ -225,7 +225,7 @@ function updateManifest() { return plugins.some((p) => p.platform === "service"); }; - var platformPath = `./${Options.pluginName}_service.js`; + var platformPath = `./AB${Options.pluginName}_service.js`; return generator.updateManifest( Options, "service", diff --git a/lib/tasks/pluginPlatformWeb.js b/lib/tasks/pluginPlatformWeb.js index f776591..a9ebe05 100644 --- a/lib/tasks/pluginPlatformWeb.js +++ b/lib/tasks/pluginPlatformWeb.js @@ -191,7 +191,7 @@ function updateManifest() { return plugins.some((p) => p.platform === "web" && p.type === typeValue); }; - var platformPath = `./${Options.pluginName}_web.mjs`; + var platformPath = `./AB${Options.pluginName}_web.mjs`; return generator.updateManifest( Options, "web", diff --git a/lib/tasks/pluginView.js b/lib/tasks/pluginView.js new file mode 100644 index 0000000..e12b05c --- /dev/null +++ b/lib/tasks/pluginView.js @@ -0,0 +1,123 @@ +// +// pluginView +// add view code (properties and web) to an existing plugin +// +// options: +// +// +var path = require("path"); +var utils = require(path.join(__dirname, "..", "utils", "utils")); +var generator = require(path.join(__dirname, "pluginPlatformGenerator")); + +var Options = {}; // the running options for this command. + +// +// Build the PluginView Command +// +var Command = new utils.Resource({ + command: "pluginView", + params: "", + descriptionShort: + "add view code (properties and web) to an existing plugin (updated architecture).", + descriptionLong: ` +`, +}); + +module.exports = Command; + +Command.help = function () { + console.log(` + + usage: $ appbuilder plugin view [pluginName] [viewName] [options] + + add view code (properties and web) to a plugin in developer/plugins/ + If the plugin doesn't exist, it will be created first. + + Options: + [pluginName] the name of the plugin to add view code to (will prompt if not provided). + [viewName] the view name for the view (e.g., "MyWidget" creates FNMyWidget.js). Defaults to pluginName if omitted. + +`); +}; + +Command.run = async function (options) { + // copy our passed in options to our Options + for (var o in options) { + Options[o] = options[o]; + } + + Options.pluginName = Options._.shift(); + Options.objectName = Options._.shift(); + // View always has type = "view" + Options.type = "view"; + + await generator.checkDependencies(); + + // Check if plugin exists, create it if it doesn't + await generator.ensurePluginExists(Options); + + await generator.findPluginDirectory(Options, "view"); + await generator.questions(Options, "view"); + + // Override plugin key for views (generator defaults to "ab-object-") + Options.pluginKey = Options.pluginKey.replace("ab-object-", "ab-view-"); + + // Copy all template files once (this will copy properties/, web/ directories and .js files) + await generator.copyTemplateFiles(Options, "pluginView"); + + // Call each platform command to handle webpack and manifest updates + // They will skip template copying since files already exist, but will update platform.js, webpack, and manifest + // Prepare options for each command with proper _ array values + + // Properties command expects: [pluginName, objectName] and sets type = "properties" + var propertiesOptions = generator.prepareCommandOptions(Options, [ + Options.pluginName, + Options.objectName, + ]); + var propertiesCommand = require(path.join( + __dirname, + "pluginPlatformProperties" + )); + await propertiesCommand.run(propertiesOptions); + + // Update properties.js to register the Editor file (main file is already registered by pluginPlatformProperties) + await updatePropertiesJs(); + + // Web command expects: [pluginName, type, objectName] + var webOptions = generator.prepareCommandOptions(Options, [ + Options.pluginName, + Options.type, + Options.objectName, + ]); + var webCommand = require(path.join(__dirname, "pluginPlatformWeb")); + await webCommand.run(webOptions); +}; + +/** + * @function updatePropertiesJs + * Update properties.js to register the Editor file using the generator's updatePlatformJs function + * @returns {Promise} + */ +function updatePropertiesJs() { + var fs = require("fs"); + var propertiesJsPath = path.join(Options.pluginDir, "properties.js"); + + if (!fs.existsSync(propertiesJsPath)) { + // If properties.js doesn't exist, pluginPlatformProperties will create it + return Promise.resolve(); + } + + // Create a temporary options object with the Editor file name + var editorOptions = {}; + for (var o in Options) { + editorOptions[o] = Options[o]; + } + editorOptions.fnObjectName = Options.fnObjectName + "Editor"; + + // Use the generator's updatePlatformJs function to add the Editor file + return generator.updatePlatformJs( + propertiesJsPath, + editorOptions, + "properties" + ); +} diff --git a/templates/pluginObject/service/FN[objectNamePascal].js b/templates/pluginObject/service/FN[objectNamePascal].js index bfb2c3d..e4d37ef 100644 --- a/templates/pluginObject/service/FN[objectNamePascal].js +++ b/templates/pluginObject/service/FN[objectNamePascal].js @@ -20,10 +20,6 @@ export default function <%= fnObjectName %>({ return "<%= pluginKey %>"; } - static getPluginType() { - return "object"; - } - /** * @method model * return a Model object that will allow you to interact with the data for diff --git a/templates/pluginObject/web/FN[objectNamePascal].js b/templates/pluginObject/web/FN[objectNamePascal].js index c4e0bb7..3340333 100644 --- a/templates/pluginObject/web/FN[objectNamePascal].js +++ b/templates/pluginObject/web/FN[objectNamePascal].js @@ -32,10 +32,6 @@ export default function <%= fnObjectName %>({ return "<%= pluginKey %>"; } - static getPluginType() { - return "object"; - } - /** * @method toObj() * diff --git a/templates/pluginView/properties/FN[objectNamePascal].js b/templates/pluginView/properties/FN[objectNamePascal].js new file mode 100644 index 0000000..1f6d86b --- /dev/null +++ b/templates/pluginView/properties/FN[objectNamePascal].js @@ -0,0 +1,88 @@ +// <%= fnObjectName %> Properties +// A properties side import for an ABView. +// +export default function <%= fnObjectName %>Properties({ + AB, + ABViewPropertiesPlugin, + // ABUIPlugin, +}) { + return class AB<%= objectNamePascal %>Properties extends ABViewPropertiesPlugin { + constructor() { + super(AB<%= objectNamePascal %>Properties.getPluginKey(), { + // + // add your property ids here: + // + }); + + this.AB = AB; + + // let myBase = ABFakeobj.getPluginKey(); + // this.UI_Credentials = FNCredentials(this.AB, myBase, ABUIPlugin); + } + + + static getPluginKey() { + return "<%= pluginKey %>"; + } + + static getPluginType() { + return "properties-view"; + // properties-view : will display in the properties panel of the ABDesigner + } + + ui() { + const ids = this.ids; + let L = this.AB.Label(); + const uiConfig = this.AB.Config.uiSettings(); + return super.ui([ + // + // insert your webix property list here + // + ]); + } + + async init(AB) { + await super.init(AB); + + // + // perform any additional initialization here + // + + } + + /** + * @method populate + * populate the properties with the values from the view. + * @param {obj} view + */ + populate(view) { + super.populate(view); + + const ids = this.ids; + + // populate your property values here + // $$(ids.height).setValue(view.settings.height); + } + + /** + * @method values + * return the values from the property editor + * @return {obj} + */ + values() { + const values = super.values(); + + const ids = this.ids; + const $component = $$(ids.component); + + values.settings = $component.getValues(); + + // make sure any additional values/settings are properly set + // if (values.settings.dataviewID == "none") + // values.settings.dataviewID = null; + + return values; + } + }; +} + diff --git a/templates/pluginView/properties/FN[objectNamePascal]Editor.js b/templates/pluginView/properties/FN[objectNamePascal]Editor.js new file mode 100644 index 0000000..79c9787 --- /dev/null +++ b/templates/pluginView/properties/FN[objectNamePascal]Editor.js @@ -0,0 +1,98 @@ +// <%= fnObjectName %> Editor +// An Editor wrapper for the ABView Component. +// The Editor is displayed in the ABDesigner as a view is worked on. +// The Editor allows a widget to be moved and placed on the canvas. +// +export default function <%= fnObjectName %>Editor({ ABViewEditorPlugin }) { + return class AB<%= objectNamePascal %>Editor extends ABViewEditorPlugin { + constructor(view, base = "interface_editor_<%= objectNamePascal.toLowerCase() %>", ids = {}) { + // view: {ABView} The ABView instance this editor is for + // base: {string} unique base id reference + // ids: {hash} { key => '' } + // this is provided by the Sub Class and has the keys + // unique to the Sub Class' interface elements. + + super(view, base, ids); + } + + /** + * @method getPluginKey + * return the plugin key for this editor. + * @return {string} plugin key + */ + static getPluginKey() { + return "<%= pluginKey %>"; + } + + /** + * @method getPluginType + * return the plugin type for this editor. + * plugin types are how our ClassManager knows how to store + * the plugin. + * @return {string} plugin type + */ + static getPluginType() { + return "editor-view"; + // editor-view : will display in the editor panel of the ABDesigner + } + + /** + * @method ui() + * Return the Webix UI definition for this editor. + * @return {object} Webix UI definition + */ + ui() { + // Default implementation uses the component's UI + // Sub classes can override this to provide custom editor UI + return super.ui(); + } + + /** + * @method init() + * Initialize the editor with the ABFactory instance. + * @param {ABFactory} AB + */ + async init(AB) { + await super.init(AB); + + // + // Add any custom initialization here + // + } + + /** + * @method onShow() + * Called when the editor is shown. + */ + onShow() { + super.onShow(); + // + // Add any custom onShow logic here + // + } + + /** + * @method onHide() + * Called when the editor is hidden. + */ + onHide() { + super.onHide(); + + // + // Add any custom onHide logic here + // + } + + /** + * @method detatch() + * Detach the editor component. + */ + detatch() { + super.detatch(); + + // + // Add any custom cleanup logic here + // + } + }; +} diff --git a/templates/pluginView/web/FN[objectNamePascal].js b/templates/pluginView/web/FN[objectNamePascal].js new file mode 100644 index 0000000..ad15390 --- /dev/null +++ b/templates/pluginView/web/FN[objectNamePascal].js @@ -0,0 +1,145 @@ +import <%= fnObjectName %>Component from "./<%= fnObjectName %>Component.js"; + + +// <%= fnObjectName %> Web +// A web side import for an ABView. +// +export default function <%= fnObjectName %>({ + /*AB,*/ + ABViewPlugin, + ABViewComponentPlugin, +}) { + const AB<%= objectNamePascal %>Component = <%= fnObjectName %>Component({ ABViewComponentPlugin }); + + + // Define the default values for this components settings: + // when a new instance of your widget is created, these values will be + // the default settings + const AB<%= objectNamePascal %>ComponentDefaults = { + // text: "", + // {string} + // A multilingual text template that is used to display a given set of + // values. + + // height: 0, + // {integer} + // The default height of this widget. + + // dataviewID: null, + // {uuid} + // The {ABDataCollection.id} of the datacollection this ABViewText is + // pulling data from. + // In most usage situations this ABView is tied to the data in an + // ABDataCollection. However, it is possible for an ABObject to be + // directly assigned to the ABView, and that will be used instead. + }; + + // Define the Default Values for this ABView + // These are used by the platform and ABDesigner to display the view. + const ABViewDefaults = { + key: "<%= pluginKey %>", + // {string} + // unique key for this view + + icon: "font", + // {string} + // fa-[icon] reference for this view + + labelKey: "Plugin <%= pluginKey %>", + // {string} + // the multilingual label key for the class label + }; + + /// + /// We return the ABView here + /// + return class AB<%= objectNamePascal %> extends ABViewPlugin { + // constructor(...params) { + // super(...params); + // } + + /** + * @method getPluginKey + * return the plugin key for this view. + * @return {string} plugin key + */ + static getPluginKey() { + return "<%= pluginKey %>"; + } + + /** + * @method common + * return the common values for this view. + * @return {obj} common values + */ + static common() { + return ABViewDefaults; + } + + /** + * @method defaultValues + * return the default values for this view. + * @return {obj} default values + */ + static defaultValues() { + return AB<%= objectNamePascal %>ComponentDefaults; + } + + /** + * @method component() + * return a UI component based upon this view. + * @return {obj} UI component + */ + component(parentId) { + return new AB<%= objectNamePascal %>Component(this, parentId); + } + + /** + * @method toObj() + * properly compile the current state of this ABView instance + * into the values needed for saving to the DB. + * @return {json} + */ + toObj() { + // NOTE: ABView auto translates/untranslates "label" + // add in any additional fields here: + // this.unTranslate(this, this, ["text"]); + + var obj = super.toObj(); + obj.views = []; + return obj; + } + + /** + * @method fromValues() + * + * initialze this object with the given set of values. + * @param {obj} values + */ + fromValues(values) { + super.fromValues(values); + + this.settings = this.settings || {}; + + // + // populate any additional fields here: + // + + // NOTE: ABView auto translates/untranslates "label" + // add in any additional fields here: + // this.translate(this, this, ["text"]); + } + + /** + * @method componentList + * return the list of components available on this view to display in the editor. + */ + componentList() { + // NOTE: if your component allows other components to be placed inside, then + // return the list of components that are allowed to be placed inside. + // otherwise return an empty array. + return []; + } + }; +} + diff --git a/templates/pluginView/web/FN[objectNamePascal]Component.js b/templates/pluginView/web/FN[objectNamePascal]Component.js new file mode 100644 index 0000000..0f75237 --- /dev/null +++ b/templates/pluginView/web/FN[objectNamePascal]Component.js @@ -0,0 +1,46 @@ +export default function <%= fnObjectName %>Component({ + /*AB,*/ + ABViewComponentPlugin, +}) { + return class AB<%= objectNamePascal %>Component extends ABViewComponentPlugin { + constructor(baseView, idBase, ids) { + super( + baseView, + idBase || `AB<%= objectNamePascal %>_${baseView.id}`, + Object.assign( + { + template: "", + }, + ids, + ), + ); + } + + /** + * @method ui + * return the Webix UI definition for this component. + * @return {object} Webix UI definition + */ + ui() { + return super.ui([ + { + id: this.ids.template, + view: "template", + template: "<%= objectNamePascal %> Template", + minHeight: 10, + // css: "ab-custom-template", + // borderless: true, + } + ]); + } + + /** + * @method onShow + * called when the component is shown. + * perform any additional initialization here. + */ + onShow() { + super.onShow(); + } + }; +}