Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions apps/generator/docs/using-private-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,42 @@ const generator = new Generator('@asyncapi/html-template', 'output',
});
```
Assuming you host `@asyncapi/html-template` in a private package registry like Verdaccio. To pull this template, you need to provide `registry.url` option that points to the registry URL and `registry.auth` as a base64 encoded value that represents the username and password. Instead of username and password, you can also pass `registry.token`.

## Using templates from private Git repositories

Generator can also use templates hosted in Git repositories, including private repositories on providers such as GitHub and GitLab.
In this case the template is referenced using a git-compatible specifier, and authentication is handled by your existing Git configuration (SSH keys, HTTPS with personal access tokens, etc.).

### CLI example

```bash
asyncapi generate fromTemplate asyncapi.yaml \
git+ssh://[email protected]/your-org/your-private-template.git \
-o output
```

You can use any git specifier supported by npm, for example:

- `git+https://github.com/your-org/your-private-template.git#v1.0.0`
- `git+ssh://[email protected]/your-org/your-private-template.git#main`
- `[email protected]:your-org/your-private-template.git`
- `github:your-org/your-private-template`

Make sure your Git credentials are configured so the underlying git client can access the repository. For example:

- Configure SSH keys that have access to the private repository.
- Use HTTPS URLs with a personal access token (PAT), following your Git provider’s documentation.

### Library example

```javascript
const generator = new Generator(
'git+https://github.com/your-org/your-private-template.git#v1.0.0',
'output',
{
debug: true
}
);
```

In both CLI and library usage, AsyncAPI Generator delegates cloning and authentication to npm and git; no additional generator-specific configuration is required beyond providing a valid git specifier and having your Git credentials set up correctly.
20 changes: 20 additions & 0 deletions apps/generator/lib/__mocks__/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,24 @@ utils.getGeneratorVersion = jest.fn(() => utils.__generatorVersion);
utils.__getTemplateDetails = {};
utils.getTemplateDetails = jest.fn(() => utils.__getTemplateDetails);

utils.__isGitSpecifierValue = false;
utils.isGitSpecifier = jest.fn((str) => {
if (utils.__isGitSpecifierValue !== false) {
return utils.__isGitSpecifierValue;
}
if (typeof str !== 'string') return false;
const trimmed = str.trim();
if (!trimmed) return false;
if (trimmed.startsWith('git+ssh://') || trimmed.startsWith('git+https://') || trimmed.startsWith('ssh://')) {
return true;
}
if (/^git@[^:]+:[^/]+\/.+/.test(trimmed)) {
return true;
}
if (trimmed.startsWith('github:') || trimmed.startsWith('gitlab:')) {
return true;
}
return false;
});

module.exports = utils;
11 changes: 9 additions & 2 deletions apps/generator/lib/generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const { listBakedInTemplates, isCoreTemplate, getTemplate } = require('./templat
const {
convertMapToObject,
isFileSystemPath,
isGitSpecifier,
readFile,
readDir,
writeFile,
Expand Down Expand Up @@ -570,7 +571,9 @@ class Generator {
* @param {Boolean} [force=false] Whether to force installation (and skip cache) or not.
*/
async installTemplate(force = false) {
if (!force) {
const isGitTemplate = isGitSpecifier(this.templateName);

if (!force && !isGitTemplate) {
let pkgPath;
let installedPkg;
let packageVersion;
Expand All @@ -595,6 +598,10 @@ class Generator {
const debugMessage = force ? logMessage.TEMPLATE_INSTALL_FLAG_MSG : logMessage.TEMPLATE_INSTALL_DISK_MSG;
log.debug(logMessage.installationDebugMessage(debugMessage));

if (isGitTemplate) {
log.debug(`Installing template from git source "${this.templateName}". Authentication is handled by your git configuration.`);
}

if (isFileSystemPath(this.templateName)) log.debug(logMessage.NPM_INSTALL_TRIGGER);

const config = new Config({
Expand Down Expand Up @@ -1111,4 +1118,4 @@ module.exports = Generator;
* @param {string} [filter.target] - Filter by target language or format (e.g., 'javascript', 'html').
* @returns {Array<Object>} Array of template objects matching the filter.
*/
module.exports.listBakedInTemplates = listBakedInTemplates;
module.exports.listBakedInTemplates = listBakedInTemplates;
36 changes: 36 additions & 0 deletions apps/generator/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,42 @@ utils.isFilePath = (str) => {
return !url.parse(str).hostname;
};

/**
* Checks if given string is a Git URL or npm git specifier.
*
* It recognizes common git forms supported by npm, including:
* - git+https://host/owner/repo.git[#ref]
* - git+ssh://git@host/owner/repo.git[#ref]
* - ssh://git@host/owner/repo.git[#ref]
* - git@host:owner/repo.git[#ref]
* - github:owner/repo[#ref], gitlab:owner/repo[#ref]
*
* @param {String} str information representing template specifier
* @returns {Boolean}
*/
utils.isGitSpecifier = (str) => {
if (typeof str !== 'string') return false;

const trimmed = str.trim();
if (!trimmed) return false;

if (trimmed.startsWith('git+ssh://')
|| trimmed.startsWith('git+https://')
|| trimmed.startsWith('ssh://')) {
return true;
}

if (/^git@[^:]+:[^/]+\/.+/.test(trimmed)) {
return true;
}

if (trimmed.startsWith('github:') || trimmed.startsWith('gitlab:')) {
return true;
}

return false;
};

/**
* Checks if given file path is JS file
*
Expand Down
11 changes: 11 additions & 0 deletions apps/generator/test/__mocks__/requireg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
/**
* Minimal mock for requireg.resolve used in tests.
* It just needs to return a string containing 'index.js'
* so that generator.js can safely call .replace('index.js','').
*/
resolve() {
return 'npm/index.js';
}
};

22 changes: 22 additions & 0 deletions apps/generator/test/generator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,28 @@ describe('Generator', () => {
expect(log.debug).toHaveBeenCalledWith(logMessage.installationDebugMessage(logMessage.TEMPLATE_INSTALL_FLAG_MSG));
});

it('works with a git specifier', async () => {
log.debug = jest.fn();
utils.__isFileSystemPathValue = false;
utils.__getTemplateDetails = undefined;
const gitTemplate = 'git+https://github.com/org/nameOfTestTemplate.git';
const gen = new Generator(gitTemplate, __dirname, {debug: true});
await gen.installTemplate();
// For git specifiers we always go through Arborist and skip local getTemplateDetails cache.

// TODO: this test is partially disabled until we find a fix for jest config in monorepo so it recognize arborist mock
// setTimeout(() => {
// expect(arboristMock.reify).toHaveBeenCalledTimes(1);
// expect(arboristMock.reify.mock.calls[0][0]).toStrictEqual({
// add: [gitTemplate],
// saveType: 'prod',
// save: false
// });
// }, 0);
expect(log.debug).toHaveBeenCalledWith(logMessage.installationDebugMessage(logMessage.TEMPLATE_INSTALL_DISK_MSG));
expect(log.debug).toHaveBeenCalledWith(expect.stringContaining('Installing template from git source'));
});

it('works with a path to registry', async () => {
log.debug = jest.fn();
const gen = new Generator('nameOfTestTemplate', __dirname, {debug: true, registry: {url: 'some.registry.com', authorizationName: 'sdfsf'}});
Expand Down
38 changes: 38 additions & 0 deletions apps/generator/test/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,44 @@ describe('Utils', () => {
});
});

describe('#isGitSpecifier', () => {
it('returns true for git+https url', () => {
expect(utils.isGitSpecifier('git+https://github.com/org/repo.git')).toBeTruthy();
});

it('returns true for git+ssh url', () => {
expect(utils.isGitSpecifier('git+ssh://[email protected]/org/repo.git')).toBeTruthy();
});

it('returns true for ssh url', () => {
expect(utils.isGitSpecifier('ssh://[email protected]/org/repo.git')).toBeTruthy();
});

it('returns true for ssh-style git@host:path', () => {
expect(utils.isGitSpecifier('[email protected]:org/repo.git')).toBeTruthy();
});

it('returns true for github shorthand', () => {
expect(utils.isGitSpecifier('github:org/repo')).toBeTruthy();
});

it('returns true for gitlab shorthand', () => {
expect(utils.isGitSpecifier('gitlab:org/repo')).toBeTruthy();
});

it('returns false for plain https url', () => {
expect(utils.isGitSpecifier('https://example.com/template.tgz')).toBeFalsy();
});

it('returns false for npm package name', () => {
expect(utils.isGitSpecifier('@asyncapi/html-template')).toBeFalsy();
});

it('returns false for file system path', () => {
expect(utils.isGitSpecifier('./my-template')).toBeFalsy();
});
});

describe('#exists', () => {
it('should return true if file exist', async () => {
const exists = await utils.exists(`${process.cwd()}/package.json`);
Expand Down