Creating Plugins



Video tutorial coming soon...


Plugins are a way to extend Vemto functionality. They can be used to perform numerous activities such as:


A) Add a new CSS theme (example: AdminLTE plugin)
B) Modify code generation (example: Multi-Tenancy plugin)
C) Modify templates (you can modify a template in the Templates Editor, and then put it in a plugin to use in all your projects)
D) Generate files that Vemto does not generate by default (eg generate a Postman collection, generate Laravel NOVA files, etc.)
E) Execute specific commands before or after code generation

What limits what plugins can do is creativity.


You can see some plugins code examples on the following links:

1) https://github.com/TiagoSilvaPereira/vemto-simple-multitenancy-plugin
2) https://github.com/TiagoSilvaPereira/vemto-admin-lte-plugin
3) https://github.com/TiagoSilvaPereira/vemto-commands-plugin
4) https://github.com/TiagoSilvaPereira/vemto-multi-tenancy-plugin


Considerations

There are some important considerations for you to know before you start creating plugins:


  • The Considerations from the Templates Editor are valid for plugins too
  • Whenever you replace a standard Vemto template via a plugin or render a plugin's template, you can access the plugin's saved data using this.pluginData in the template's VET code
  • Files can only be generated in project-relative paths. Cannot write files outside the project
  • When you generate a file on a Laravel project from a plugin, if you uninstall the plugin, the file will be removed in the next code generation
  • Likewise, commands can only be executed within the project directory
  • Even if you are working on Windows, always use "/" to separate directories, never "\". Using the second option will cause errors in using the plugin. Vemto always considers "/" as the correct slash for internal plugin operations.
  • You can install node.js packages in your plugins. For example, if you want to show a chart in your plugin's Vue component, you can install chart.js or something similar and use it (however this should be done with care, and maliciously crafted plugins will be blocked)
  • With big powers come big responsibilities. We are not responsible if you enter potentially dangerous commands into your plugin
  • Avoid installing third-party plugins that are not on the Marketplace, unless you know the origin
  • We reserve the right to block plugins from being installed if they contain malicious code or inappropriate content

Requirements

Before starting to create your Vemto plugin, please be sure to install:


Downloading the base

The first step is downloading the Plugin Base from the Vemto Marketplace:


After that, extract the ZIP file to the /plugins folder, inside the Vemto data path, and rename the extracted folder to match your plugin name (see the plugin names convention right below).


By default, you can find this folder under the following directories:


  • on Linux: ~/.config/vemto/plugins
  • on macOS: ~/Library/Application Support/vemto/plugins
  • on Windows: %USERPROFILE%\AppData\Roaming\vemto\plugins


If the /plugins path does not exist, you can create it manually (it means you never installed a plugin on Vemto before).


This extracted folder would be your plugin folder, and now you can start modifying it to have the wanted behavior.

Git Repository

If you want to publish your plugin in the future on Vemto Marketplace, you need to create a GitHub repository. We recommend naming your repository with the following pattern:


vemto-[PLUGIN-NAME-HERE]-plugin


For example:


- vemto-adminlte-plugin
- vemto-laravel-nova-plugin


Each plugin version must have a separate branch. For example, version 1.0.0 is in the 1.0.0 branch (Vemto will use the branch name to download the file via the marketplace). Versioning must be done using the SEMVER pattern, with three digits separated by dots.


It is also recommended to always keep the latest plugin version as the default branch on GitHub.

README.md

The README.md file is very important, as it will show the instructions on how to use your plugin, and it will be shown on the Vemto marketplace.


You can see some examples here:


Configuration

Now, let's configure the package.json file to reflect the plugin data. It's a very simple and intuitive file, and it's a standard Node.js module file. Only a few settings are specific to Vemto, such as title, minVemtoVersion, and image.


A very important piece of data is the plugin id, especially when distributing it on the marketplace. The recommended pattern for creating it is to use its domain or inverted name at first, and then the plugin name. Some examples:


app.vemto.plugin-base // vemto.app
rodrigues.tiago.multitenancy // Tiago Rodrigues
app.vemto.adminlte // vemto.app


Another important data is minVemtoVersion, which defines the minimum Vemto version for plugin installation. This is very useful for creating plugins that use newer API methods, preventing them from being installed in versions that cannot execute them.


Here you can see the complete package.json file for a new plugin (the image field can be kept null for now, it is not used in Vemto yet, but it will be in the future).


{
    "version": "1.0.0",
    "id": "app.vemto.plugin-base",
    "name": "plugin-base",
    "title": "Plugin Base",
    "description": "A base for creating Vemto plugins",
    "image": "",
    "main": "src/main.js",
    "vueComponent": "dist/plugin-base.umd.min.js",
    "repository": {
        "type": "git",
        "url": "https://github.com/TiagoSilvaPereira/vemto-plugin-base"
    },
    "minVemtoVersion": "1.0.0"
}


There are two other very important items on the package.json file:


"main": "src/main.js",
"vueComponent": "dist/plugin-base.umd.min.js",


Both will be better explained in the section below.

{danger} Plugins are, in essence, node.js packages and therefore support installing third-party modules usually with npm install or yarn install. However, be careful when choosing packages if you want to distribute your plugin on the Marketplace, as problematic and/or malicious packages will not be accepted.

The main.js file

The src/main.js file is responsible for controlling what the plugin will do at certain times during its execution. Each of these moments is called a Hook, and they correspond to methods that can be called inside main.js.


As you can see, this file always receives an instance of the Vemto API, which will be used later, inside the Hooks, to interact with Vemto:


module.exports = (vemto) => {
// ...


Most of the time, your plugin will use this file a lot, in conjunction with the Vue component explained in the next section. But in some types of plugins, where you will only read the project data and present it visually using the Vue component (for example, a plugin that shows the number of models, cruds, and files of a project in the same location), this file does not will be used a lot. In this case, you can keep it in its simplest version, just checking the installation authorization using the canInstall Hook:


module.exports = (vemto) => {

    return {

        canInstall() {
            return true
        },

    }

}


You can declare all Hooks you need to use inside your plugin main.js file like below:


module.exports = (vemto) => {

    return {

        canInstall() {
            // ...
        },

        onInstall() {
            // ...
        },

        beforeCodeGenerationStart() {
            // ...
        },

        // Etc...

    }

}

{info} After changing the main.js file, it may be necessary to perform a "Reload" of the plugin in Vemto so that the cache is cleared and the changes take effect.

Now let's get to know the Hooks available for use within main.js:

canInstall

Defines if the plugin can be installed. When a specific condition is not met, it should return false and use the addBlockReason API method to display a message on the lock screen. If your plugin doesn't have installation conditions, just return true. Example:


canInstall() {
    let projectSettings = vemto.getProjectSettings()

    if(projectSettings.cssFramework !== 'bootstrap') {
        vemto.addBlockReason('This plugin is only compatible with Bootstrap projects')
        return false
    }

    if(projectSettings.uiTemplate !== 'laravel_ui') {
        vemto.addBlockReason('This plugin is only compatible with Laravel UI templates')
        return false
    }

    return true
}

onInstall

Run something as soon as the plugin is installed. It is usually used to save the initial plugin data in the project (this data will be available for all plugin Hooks and in the Vue component using the getPluginData API method). Example:


onInstall() {
    vemto.savePluginData({
        sidebarMode: 'dark',
        sidebarCollapsed: true,
    })
},

composerPackages

Allows to edit the composer.json file as a Javascript Object. It is util to install/uninstall composer packages during the code generation. Example:


composerPackages(packages) {
    // Adding packages
    packages.requireDev['phpunit/phpunit'] = '^9.5'
    packages.require['spatie/laravel-multitenancy'] = '^1.0'

    // Removing packages
    delete packages.require['spatie/laravel-multitenancy']

    return packages
},

{info} This method was added on Vemto 1.0.1

nodePackages

Allows to edit the packages.json file as a Javascript Object. It is util to install/uninstall Node packages during the code generation. Example:


nodePackages(packages) {

    // Adding packages
    packages.dependencies['moment'] = '^2.0'
    packages.devDependencies['bootstrap'] = '^4.0.0'

    // Removing packages
    delete packages.devDependencies['bootstrap']

    return packages
},

{info} This method was added on Vemto 1.0.1

beforeCodeGenerationStart

Executes something just before code generation starts (for example a command, or the writing of some important file, or even a message in the log). Some Examples:


beforeCodeGenerationStart() {
    vemto.log.info('Starting code generation...')
},
// Checking if there is an irregular tenancy model and aborting the code generation
beforeCodeGenerationStart() {
    let data = vemto.getPluginData(),
        models = vemto.getProjectModels()

    models.forEach(model => {
        if(this.isModelOwnedByTenant(model) && !model.hasFieldByName(data.tenantFieldName)) {
            vemto.log.error(`[TENANCY ERROR] Model ${model.name} does not have a field ${data.tenantFieldName}`)
            vemto.generator.abort()
        }
    })
},

afterCodeGenerationStart

Works basically the same way as beforeCodeGenerationStart, but it runs right after code generation starts. Example:


afterCodeGenerationStart() {
    vemto.log.info('Code generation already started...')
},

beforeRender{TemplateName}

This is a very important Hook, as it allows you to manipulate the code generation of a template right before it is rendered and turned into a file.


It runs for all templates, and always follows the pattern beforeRender{TemplateName}. For example:


beforeRenderModel
beforeRenderController
beforeRenderAuthServiceProvider
beforeRenderIndexView


If you have doubts about the name of the method for a certain template, you can view it from the Template Editor, as shown in the image below:


image


This Hook always receives two parameters:


  • template - An instance of Template (you can get the data injected into the VET template using the template.getData() method)
  • content - The compiled template content, which you can manipulate before rendering

{info} In case you are confused about the difference between compiled and rendered templates... we call compiled the template that has already gone through the VET, and rendered, the one that already has the final content to be transformed into a file

Let's illustrate the use. Assuming you want to add a PHP class to all controllers generated by Vemto, you could do something like:

beforeRenderController(template, content) {
    let phpFile = vemto.parsePhp(content)

    phpFile.addUseStatement('App\\MyClass')

    return phpFile.getCode()
}

templateReplacements

This Hook is called at the right time so that you can change Vemto's default templates using the replaceTemplate API method.


templateReplacements() {
    vemto.log.message('Replacing stubs for AdminLTE...')

    // Replace a Vemto default template
    vemto.replaceTemplate(`Model.vemtl`, 'files/Model.vemtl')
}

copyableFiles

You can use this Hook to copy files into the Laravel project generated by Vemto. It must return an array of objects, where each object has the following properties:


  • from - indicates the file that should be copied (must be inside the plugin's directory, in a folder of your choice, for example, /files)
  • to - the final file that will be generated, with the path relative to the Laravel project


Example:


copyableFiles() {
    return [
        {from: 'files/MyClass.php', to: 'app/Classes/MyClass.php'},
        {from: 'files/MyOtherClass.php', to: 'app/Classes/MyOtherClass.php'},
    ]
}

beforeCodeGenerationEnd

Executes something just before code generation ends (for example a command, or the writing of some important file, or even a message in the log). Some Examples:


beforeCodeGenerationEnd() {
    vemto.log.info('Code generation will finish...')
},
beforeCodeGenerationEnd() {
    vemto.executeCommand('php artisan migrate')
},
beforeCodeGenerationEnd() {
    let options = {
        formatAs: 'php'
    }

    vemto.log.message('Generating tenancy files...')

    vemto.renderTemplate('files/TenantScope.vemtl', 'app/Tenancy/TenantScope.php', options)
}

afterCodeGenerationEnd

Executes something just before code generation ends (for example a command, or the writing of some important file, or even a message in the log). Some Examples:


afterCodeGenerationEnd() {
    vemto.log.info('Code generation finished...')
},

addEntityMarkers

This Hook is only used to add marks to Schema Editor entities, using the addEntityMarker API method. Example:


addEntityMarkers() {
    let models = vemto.getProjectModels()

    models.forEach(model => {
        if(model.name !== 'User') return

        vemto.addEntityMarker(model, 'User Model', '#ECC94B')
    })
},


This code will add the following tag:

image


beforeRunnerStart

Executes something just before running the project (for example, if you want to start another server, or migrate something from a custom package). Example:


beforeRunnerStart() {
    vemto.log.info('Will run the project...')

    vemto.executeArtisan('migrate:fresh')
},


This hook is also important if you want to disable the default commands that are fired when you run the project:


beforeRunnerStart() {
    // Disable the php artisan serve command 
    vemto.disableDefaultRunnerServer()

    // Disable the php artisan migrate command
    vemto.disableDefaultRunnerMigrations()

    // Disable opening the application url on the browser
    vemto.disableDefaultRunnerWebPageTrigger()
},

{info} This method was added on Vemto 1.0.1


beforeRunnerEnd

Executes something just before finishing to run the project. Example:


beforeRunnerEnd() {
    vemto.openLink('http://localhost:8000')
},

{info} This method was added on Vemto 1.0.1

The Vue component

A plugin's Vue component (usually Component.vue) is used to create a visual interface for the plugin. If your plugin doesn't need a visual interface, you can simply add a simple message like "Plugin installed and working..." to this component.


It's a common Vue component as you may already know, but it has access to some features of the Vemto API.


Below, let's learn about some of these features:

TailwindCSS

Vemto uses the TailwindCSS framework to build its entire interface. By creating a visual interface for your plugin, you will automatically be using it. However, there are some ready-made classes according to the Vemto interface that you can use:


  • For cards: .card, .item-card
  • For inputs: .input, .small-input, .xs-input
  • For buttons: .button, .button-primary, .button-secondary, .button-danger
  • For checkboxes: .form-checkbox
  • For radiobuttons: .form-radio


{info} Maintaining the plugin's visual consistency with the Vemto interface makes it easier to approve it for the Marketplace.

Vemto API

The API is also available within the Vue component. However, since the visual part of the plugin does not have access to Hooks, it is not possible to use methods to generate or replace files, or to run on other important Hooks such as addEntityMarkers.


Tasks like: changing files and templates, writing files, etc. are always done in main.js.


One of the most common uses of the API within the Vue component is to get the data through the getPluginData method and to save it later using the savePluginData method.


Example:


data() {
    return {
        text: {},
    }
},

created() {
    let pluginData = window.vemtoApi.getPluginData()
    this.text = pluginData.sidebarMode
},

methods: {
    save() {
        window.vemtoApi.savePluginData({
            text: this.text,
        })
    }
}


Another common use is to debounce when saving data (for example, when you have an input and want to save the data while someone is typing). In order not to have to install a debounce library, Vemto already makes it available for you:


methods: {
    // 500 ms debounce to save the data
    save: window.vemtoApi.debounce(function() {
        window.vemtoApi.savePluginData({
            text: this.text
        })
    }, 500)
}

{info} Always place a debounce if you want to save data as you type. This will facilitate the approval of your plugin on the Marketplace, and it helps a lot in the general performance of Vemto.

Building the component

While creating your component, and after, publishing the component, you will need to compile it so that it will be recognized by Vemto. It is not possible to directly include the Component.vue file, it must first go through a process that transforms it into a library.

{warning} Due to the new Vue CLI (@vue/cli) deprecating the old vue-cli, the command vue build ./src/Component.vue -t lib will not work to build the component, unless you are still using the old Vue CLI (in that case, just run this command inside the plugin folder). So, if you are using newest Vue CLI versions, please follow the steps below.


Firstly, install the new Vue CLI and the Vue CLI Service globally:

-- Removing the old Vue CLI 
npm uninstall vue-cli -g

-- Installing Vue CLI globally
npm install -g @vue/cli

-- Installing the Vue CLI service
npm install -g @vue/cli-service

-- Installing the Vue template compiler globally (For Vue 2)
npm i -g vue-template-compiler

-- Installing the Vue template compiler globally (For Vue 3)
npm i -g @vue/compiler-sfc


After installing all the necessary stuff, just run the following command inside your plugin folder:


vue-cli-service build ./src/Component.vue --target lib --inline-vue


As you can see, we are using the --target lib argument. This allows third-party packages included in Component.vue to be compiled along with your code, making it possible to create plugins that go beyond the possibilities of the Vemto API.


After the first compilation, be sure to add the compiled and minified component that is in the /dist folder in your package.json settings, if you haven't already:


{
    "vueComponent": "dist/my-plugin.umd.min.js"
}

{info} After a compilation, it may be necessary to perform a "Reload" of the plugin in Vemto so that the cache is cleared and the changes take effect.

Vue component debug

If you want to show messages while creating your plugin component, you may use the Plugin Console to do it.


image


You can access the console methods using the window.vemtoApi.pluginConsole helper inside your Vue components. For example:

window.vemtoApi.pluginConsole.log('Some message here')
window.vemtoApi.pluginConsole.error('Something went wrong')

// The .error method supports both errors and string messages
try {
    // ...
} catch (error) {
    window.vemtoApi.pluginConsole.error(error)
    window.vemtoApi.pluginConsole.error('string message')
}

Publish on the Marketplace

While we don't have a plugin submission form yet, you can send an email to [email protected] with the link to your GitHub repository for us to review and add your plugin to the Marketplace.


To date, we do not support paid plugins. If you are interested in distributing a plugin for a fee, please contact us so we can discuss the options.