Search code examples
jhipsteryeomanblueprint

How to create a custom blueprint?


I'm trying to create a customized JHipster blueprint for my organization.

I've started my journey:

  1. Installed Yeoman v4.3.0
  2. Installed Jhipster v7.9.3
  3. Created a directory for my future blueprint mkdir mygenerator && cd mygenerator
  4. Executed the command to create a new blueprint: jhipster generate-blueprint
    • selected only the sub-generator server
    • add a cli: Y
    • Is server generator a side-by-side blueprint: Y
    • Is server generator a cli command: N
    • selected the tasks: initializing, prompting and configuring

From this point, I've opened the generated blueprint project with VS Code and noticed a first problem, some jhipster packages can't be resolved:

  • Unable to resolve path to module 'generator-jhipster/esm/generators/server'
  • Unable to resolve path to module 'generator-jhipster/esm/priorities'

I also noticed that the generator created for me has a small difference from the existing generators in the JHipster Github, such as jhipster-dotnetcore, generator-jhipster-quarkus, generator-jhipster-nodejs: the returned functions are async while in the cited repos they are regular functions (sync):

get [INITIALIZING_PRIORITY]() {
        return {
            async initializingTemplateTask() {},
        };
    }

Does it make any difference in this Jhipster version or there is no problem if I return the same way as jhipster-dotnetcore:

get initializing() {
        return {
            ...super._initializing(),
            setupServerConsts() {
                this.packagejs = packagejs;
            ...

I've assumed that this detail is not important and followed with async function and write my prompting function to get some input from the user/developer in order to replace values in the template files :

get [PROMPTING_PRIORITY]() {
        return {
            ...super._prompting(),
            async promptingTemplateTask() {
                const choices = [
                    {
                        name: 'OAuth 2.0 Protocol',
                        value: 'oauth2',
                    },
                    {
                        name: 'CAS Protocol',
                        value: 'cas',
                    },
                ];

                const PROMPTS = {
                    type: 'list',
                    name: 'authenticationProtocol',
                    message: 'Which authentication protocol do you want to use?',
                    choices,
                    default: 'oauth2',
                };

                const done = this.async();

                if (choices.length > 0) {
                    this.prompt(PROMPTS).then(prompt => {
                        this.authenticationProtocol = this.jhipsterConfig.authenticationProtocol = prompt.authenticationProtocol;
                        done();
                    });
                } else {
                    done();
                }
            },
        };
    }

<%_ if (authenticationProtocol == 'oauth2') { _%>
    security:
        enable-csrf: true
        oauth2:
            client:
                clientId: ${this.baseName}
                clientSecret: Z3ByZXBmdGVy
                accessTokenUri: http://localhost:8443/oauth2.0/accessToken
                userAuthorizationUri: http://localhost:8443/oauth2.0/authorize
                tokenName: oauth_token
                authenticationScheme: query
                clientAuthenticationScheme: form
                logoutUri: http://localhost:8443/logout
                clientSuccessUri: http://localhost:4200/#/login-success
            resource:
                userInfoUri: http://localhost:8443/oauth2.0/profile
<%_ } _%>
    thymeleaf:
        mode: HTML

templates/src/test/java/resources/config/application.yml.ejs

All this done, I've followed the next steps:

  1. Run npm link inside the blueprint directory.
  2. Created a new directory for a app example: mkdir appmygenerator && cd appmygenerator
  3. Started a new example app with my blueprint: jhipster --blueprint mygenerator --skip-git --skip-install --skip-user-management --skip-client answering all question.

Here I've got some surprises:

  1. After answering What is the base name of your application? I've got this warning: [DEP0148] DeprecationWarning: Use of deprecated folder mapping "./lib/util/" in the "exports" field module resolution of the package at /...<my-generator-path>/node_modules/yeoman-environment/package.json. Update this package.json to use a subpath pattern like "./lib/util/*"
  2. My prompting function somehow made some questions be repeated, from question Do you want to make it reactive with Spring WebFlux? until Which other technologies would you like to use?.
  3. When my prompt was finally shown, there was a message in front of the last option: CAS Protocol Run-async wrapped function (sync) returned a promise but async() callback must be executed to resolve

I've made some changes to my prompt function: removed the calling of super._prompting() with the hope to solve the item 2, and removed the async in the hope to solve the item 3.

Well ... apparently it was solved. But I get a new error when JHipster (or Yeoman) try process the template:

 An error occured while running jhipster:server#writeFiles
ERROR! /home/fabianorodrigo/Downloads/my-blueprint/generators/server/templates/src/test/resources/config/application.yml.ejs:47
    45|         favicon:
    46|             enabled: false
 >> 47| <%_ if (authenticationProtocol == 'oauth2') { _%>
    48|     security:
    49|         enable-csrf: true
    50|         oauth2:

authenticationProtocol is not defined

How come authenticationProtocol is not defined? I'm stuck here. What I could noticed is that, in all the Jhipster's generators I've cited above, the prompt function sets the properties like "this.[property] = [value]" and the "this.jhipsterConfig.[property] = [value]" and in the templates they are referenced (just the property's name) and it works.

What am I missing? Why even if I set the property "this.authenticationProtocol" in the function prompting it is not seem at the template?


Solution

    • Yeoman (yo/yeoman-generator/yeoman-environment) are not required and should no be a dependency to avoid duplication in the dependency tree, unless you know what you are doing. JHipster customizes them, yeoman-test is required by tests.
    • Unable to resolve path to module is a bug at eslint-plugin-import
    • I also noticed that the generator created for me has a small difference from the existing generators in the JHipster Github, such as jhipster-dotnetcore, generator-jhipster-quarkus, generator-jhipster-nodejs. Those blueprints are quite old (blueprint support is changing very fast for v8/esm) and are full server/backend replacements, seems you are trying to add cas support. The use case is quite different.
    • Does it make any difference in this Jhipster version or there is no problem if I return the same way as jhipster-dotnetcore? Yes, get [INITIALIZING_PRIORITY]() is the new notation, and INITIALIZING_PRIORITY may be >initializing instead of initializing. The explanation is here. JHipster v8 will not support the old notation.
    • ...super._prompting(), is used to ask original prompts, since this is a side-by-side blueprint, prompts will be duplicated.
    • [DEP0148] DeprecationWarning: Use of deprecated folder mapping "./lib/util/" is a bug in yeoman-environment, and should be fixed in next version.
    • CAS Protocol Run-async wrapped function (sync) returned a promise but async() callback must be executed to resolve is shown because you are using async function with const done = this.async(); done(); together. this.async() is a to support async through callbacks before Promises were a js default.

    There are a few blueprints that uses new notation and can be used as inspiration: native, ionic, jooq and entity-audit.

    I didn't see anything about the writing priority, so it looks like you are overriding an existing template and the original generator will write it. For this reason you should inject you configuration into the original generator.

    The end result should be something like:

        get [INITIALIZING_PRIORITY]() {
            return {
                async initializingTemplateTask() {
                    this.info('this blueprint adds support to cas authentication protocol');
                },
            };
        }
    
        get [PROMPTING_PRIORITY]() {
            return {
                async promptingTemplateTask() {
                    await this.prompt({
                        type: 'list',
                        name: 'authenticationProtocol',
                        message: 'Which authentication protocol do you want to use?',
                        choices: [
                            {
                                name: 'OAuth 2.0 Protocol',
                                value: 'oauth2',
                            },
                            {
                                name: 'CAS Protocol',
                                value: 'cas',
                            },
                        ],
                        default: 'oauth2',
                    }, this.blueprintStorage); // <- `this.blueprintStorage` tells the prompt function to store the configuration inside `.yo-rc.json` at the blueprint namespace.
                },
            };
        }
    
        get [CONFIGURING_PRIORITY]() {
            return {
                configuringTemplateTask() {
                    // Store the default configuration
                    this.blueprintConfig.authenticationProtocol = this.blueprintConfig.authenticationProtocol || 'oauth2';
                },
            };
        }
    
        get [LOADING_PRIORITY]() {
            return {
                loadingTemplateTask() {
                    // Load the stored configuration, the prompt can be skipped so this needs to be in another priority.
                    this.authenticationProtocol = this.blueprintConfig.authenticationProtocol;
    
                    // Inject the configuration into the original generator. If you are writing the template by yourself, this may be not necessary.
                    this.options.jhipsterContext.authenticationProtocol = this.blueprintConfig.authenticationProtocol;
                },
            };
        }