Skip to content

Commit

Permalink
Allow generator to pull private registry
Browse files Browse the repository at this point in the history
  • Loading branch information
aayushRedHat committed Jan 17, 2024
1 parent 74173a7 commit e95321d
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 46 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/test-private-registry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Testing Private Registry

on: ['pull_request']
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
super-cli: ${{ steps.set-crashers-count.outputs.super-cli }}
name: ci
services:
verdaccio:
image: verdaccio/verdaccio:5
ports:
- 4873:4873
steps:
- uses: AutoModality/action-clean@v1
- uses: actions/[email protected]
- name: Use Node
uses: actions/setup-node@v2
with:
node-version: 16
- name: Install
run: npm install
- name: Installing login
run: npm install -g npm-cli-login
- name: Test
run: npm test
- name: credentials
run: npm run login
- name: Test Registry
run: npm run test:registry
working-directory: ./test/test-project
5 changes: 1 addition & 4 deletions lib/__mocks__/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
const utils = jest.genMockFromModule('../utils');
const { getInvalidOptions } = require.requireActual('../utils');

utils.getInvalidOptions = getInvalidOptions;

utils.__files = {};
utils.readFile = jest.fn(async (filePath) => {
Expand All @@ -25,4 +22,4 @@ utils.getGeneratorVersion = jest.fn(() => utils.__generatorVersion);
utils.__getTemplateDetails = {};
utils.getTemplateDetails = jest.fn(() => utils.__getTemplateDetails);

module.exports = utils;
module.exports = utils;
86 changes: 68 additions & 18 deletions lib/generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ const {
copyFile,
exists,
fetchSpec,
getInvalidOptions,
isReactTemplate,
isJsFile,
registerSourceMap,
Expand All @@ -44,8 +43,7 @@ const DEFAULT_TEMPLATES_DIR = path.resolve(ROOT_DIR, 'node_modules');

const TRANSPILED_TEMPLATE_LOCATION = '__transpiled';
const TEMPLATE_CONTENT_DIRNAME = 'template';
const GENERATOR_OPTIONS = ['debug', 'disabledHooks', 'entrypoint', 'forceWrite', 'install', 'noOverwriteGlobs', 'output', 'templateParams', 'mapBaseUrlToFolder'];

const GENERATOR_OPTIONS = ['debug', 'disabledHooks', 'entrypoint', 'forceWrite', 'install', 'noOverwriteGlobs', 'output', 'templateParams', 'mapBaseUrlToFolder', 'url', 'auth', 'token', 'registry'];
const logMessage = require('./logMessages');

const shouldIgnoreFile = filePath =>
Expand Down Expand Up @@ -86,14 +84,21 @@ class Generator {
* @param {Boolean} [options.install=false] Install the template and its dependencies, even when the template has already been installed.
* @param {Boolean} [options.debug=false] Enable more specific errors in the console. At the moment it only shows specific errors about filters. Keep in mind that as a result errors about template are less descriptive.
* @param {Object<String, String>} [options.mapBaseUrlToFolder] Optional parameter to map schema references from a base url to a local base folder e.g. url=https://schema.example.com/crm/ folder=./test/docs/ .
* @param {Object} [options.registry] Optional parameter with private registry configuration
* @param {String} [options.registry.url] Parameter to pass npm registry url
* @param {String} [options.registry.auth] Optional parameter to pass npm registry username and password encoded with base64, formatted like username:password value should be encoded
* @param {String} [options.registry.token] Optional parameter to pass npm registry auth token that you can grab from .npmrc file
*/
constructor(templateName, targetDir, { templateParams = {}, entrypoint, noOverwriteGlobs, disabledHooks, output = 'fs', forceWrite = false, install = false, debug = false, mapBaseUrlToFolder = {} } = {}) {
const invalidOptions = getInvalidOptions(GENERATOR_OPTIONS, arguments[arguments.length - 1] || []);
if (invalidOptions.length) throw new Error(`These options are not supported by the generator: ${invalidOptions.join(', ')}`);

constructor(templateName, targetDir, { templateParams = {}, entrypoint, noOverwriteGlobs, disabledHooks, output = 'fs', forceWrite = false, install = false, debug = false, mapBaseUrlToFolder = {}, registry = {}} = {}) {
const options = arguments[arguments.length - 1];
this.verifyoptions(options);
if (!templateName) throw new Error('No template name has been specified.');
if (!entrypoint && !targetDir) throw new Error('No target directory has been specified.');
if (!['fs', 'string'].includes(output)) throw new Error(`Invalid output type ${output}. Valid values are 'fs' and 'string'.`);

/** @type {Object} Npm registry information. */
this.registry = registry;
/** @type {String} Name of the template to generate. */
this.templateName = templateName;
/** @type {String} Path to the directory where the files will be generated. */
Expand Down Expand Up @@ -136,14 +141,31 @@ class Generator {
});
}

/**
* Check if the Registry Options are valid or not.
*
* @private
* @param {Object} invalidRegOptions Invalid Registry Options.
*
*/

verifyoptions(Options) {
if (typeof Options !== 'object') return [];
const invalidOptions = Object.keys(Options).filter(param => !GENERATOR_OPTIONS.includes(param));

if (invalidOptions.length > 0) {
throw new Error(`These options are not supported by the generator: ${invalidOptions.join(', ')}`);
}
}

/**
* Generates files from a given template and an AsyncAPIDocument object.
*
* @async
* @example
* await generator.generate(myAsyncAPIdocument);
* console.log('Done!');
*
*
* @example
* generator
* .generate(myAsyncAPIdocument)
Expand Down Expand Up @@ -201,9 +223,9 @@ class Generator {
* @example
* const generator = new Generator();
* await generator.setupOutput();
*
*
* @async
*
*
* @throws {Error} If 'output' is set to 'string' without providing 'entrypoint'.
*/
async setupOutput() {
Expand Down Expand Up @@ -287,11 +309,9 @@ class Generator {
await this.parseInput(this.asyncapi, parseOptions);
validateTemplateConfig(this.templateConfig, this.templateParams, this.asyncapi);
await this.configureTemplate();

if (!isReactTemplate(this.templateConfig)) {
await registerFilters(this.nunjucks, this.templateConfig, this.templateDir, FILTERS_DIRNAME);
}

await registerHooks(this.hooks, this.templateConfig, this.templateDir, HOOKS_DIRNAME);
await this.launchHook('generate:before');
}
Expand All @@ -313,11 +333,9 @@ class Generator {
async handleEntrypoint() {
if (this.entrypoint) {
const entrypointPath = path.resolve(this.templateContentDir, this.entrypoint);

if (!(await exists(entrypointPath))) {
throw new Error(`Template entrypoint "${entrypointPath}" couldn't be found.`);
}

if (this.output === 'fs') {
await this.generateFile(this.asyncapi, path.basename(entrypointPath), path.dirname(entrypointPath));
await this.launchHook('generate:after');
Expand Down Expand Up @@ -502,6 +520,33 @@ class Generator {
return await readFile(path.resolve(templatesDir, templateName, filePath), 'utf8');
}

/**
* @private
* @param {Object} arbOptions ArbOptions to intialise the Registry details.
*/
initialiseArbOptions(arbOptions) {
let registryUrl = 'registry.npmjs.org';
let authorizationName = 'anonymous';
const providedRegistry = this.registry.url;

if (providedRegistry) {
arbOptions.registry = providedRegistry;
registryUrl = providedRegistry;
}

const domainName = registryUrl.replace(/^https?:\/\//, '');
//doing basic if/else so basically only one auth type is used and token as more secure is primary
if (this.registry.token) {
authorizationName = `//${domainName}:_authToken`;
arbOptions[authorizationName] = this.registry.token;
} else if (this.registry.auth) {
authorizationName = `//${domainName}:_auth`;
arbOptions[authorizationName] = this.registry.auth;
}

//not sharing in logs neither token nor auth for security reasons
log.debug(`Using npm registry ${registryUrl} and authorization type ${authorizationName} to handle template installation.`);
}
/**
* Downloads and installs a template and its dependencies
*
Expand All @@ -526,7 +571,7 @@ class Generator {
path: pkgPath
});
} catch (e) {
log.debug(logMessage.packageNotAvailable(pkgPath), e);
log.debug(logMessage.packageNotAvailable(installedPkg), e);
// We did our best. Proceed with installation...
}
}
Expand All @@ -536,9 +581,14 @@ class Generator {

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

const arb = new Arborist({
path: ROOT_DIR
});
const arbOptions = {
path: ROOT_DIR,
};
if (this.registry) {
this.initialiseArbOptions(arbOptions);
}
console.log(arbOptions);
const arb = new Arborist(arbOptions);

try {
const installResult = await arb.reify({
Expand Down Expand Up @@ -1028,4 +1078,4 @@ class Generator {
Generator.DEFAULT_TEMPLATES_DIR = DEFAULT_TEMPLATES_DIR;
Generator.TRANSPILED_TEMPLATE_LOCATION = TRANSPILED_TEMPLATE_LOCATION;

module.exports = Generator;
module.exports = Generator;
8 changes: 6 additions & 2 deletions lib/logMessages.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ function templateNotFound(templateName) {
return `${templateName} not found in local dependencies but found it installed as a global package.`;
}

function packageNotAvailable(pkgPath) {
return `Unable to resolve template location at ${pkgPath}. Package is not available locally.`;
function packageNotAvailable(packageDetails) {
if (packageDetails && packageDetails.pkgPath) {
return `Unable to resolve template location at ${packageDetails.pkgPath}. Package is not available locally.`;
}

return `Template is not available locally and expected location is undefined. Known details are: ${JSON.stringify(packageDetails, null, 2)}`;
}

function installationDebugMessage(debugMessage) {
Expand Down
13 changes: 1 addition & 12 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,6 @@ utils.getGeneratorVersion = () => {
return packageJson.version;
};

/**
* Filters out the Generator invalid options given
*
* @param {Array}
* @returns {Array}
*/
utils.getInvalidOptions = (generatorOptions, options) => {
if (typeof options !== 'object') return [];
return Object.keys(options).filter(param => !generatorOptions.includes(param));
};

/**
* Determine whether the given function is asynchronous.
* @private
Expand Down Expand Up @@ -231,4 +220,4 @@ utils.convertCollectionToObject = (array, idFunction) => {
tempObject[id] = value;
}
return tempObject;
};
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"npm": ">6.13.7"
},
"scripts": {
"login": "NPM_REGISTRY=http://localhost:4873 NPM_USER=admin NPM_PASS=nimda [email protected] npm-cli-login",
"test": "npm run test:unit && npm run test:integration && npm run test:cli",
"test:unit": "jest --coverage --testPathIgnorePatterns=integration --testPathIgnorePatterns=test-project",
"test:dev": "npm run test:unit -- --watchAll",
Expand Down
21 changes: 19 additions & 2 deletions test/generator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const path = require('path');
const Generator = require('../lib/generator');
const log = require('loglevel');
const unixify = require('unixify');

const dummyYAML = fs.readFileSync(path.resolve(__dirname, './docs/dummy.yml'), 'utf8');

const logMessage = require('./../lib/logMessages.js');
Expand Down Expand Up @@ -415,6 +414,24 @@ describe('Generator', () => {
expect(arboristMock.reify).toHaveBeenCalledTimes(1);
}, 0);
});

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'}});
await gen.installTemplate();
setTimeout(() => { // This puts the call at the end of the Node.js event loop queue.
expect(arboristMock.reify).toHaveBeenCalledTimes(1);
});
});

it('throws an error indicating an unexpected param was given for registry configuration', () => {
const t = () => new Generator('testTemplate', __dirname, {
url: 'some.url.com',
privateKey: 'some.key'

});
expect(t).toThrow('These options are not supported by the generator: privateKey');
});
});

describe('.getTemplateFile', () => {
Expand Down Expand Up @@ -594,4 +611,4 @@ describe('Generator', () => {
expect(gen.isHookAvailable('string-test-hooks')).toStrictEqual(false);
});
});
});
});
8 changes: 0 additions & 8 deletions test/test-project/docker-compose.yml

This file was deleted.

48 changes: 48 additions & 0 deletions test/test-project/test-registry.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @jest-environment node
*/

const { readFile } = require('fs').promises;
const path = require('path');
const Generator = require('@asyncapi/generator');
const dummySpecPath = path.resolve(__dirname, '../docs/dummy.yml');
const crypto = require('crypto');
const mainTestResultPath = 'test/temp/integrationTestResult';
process.env['PUPPETEER_SKIP_CHROMIUM_DOWNLOAD'] = true;

describe('Integration testing generateFromFile() to make sure the template can be download from the private repository.', () => {
const generateFolderName = () => {
//you always want to generate to new directory to make sure test runs in clear environment
return path.resolve(mainTestResultPath, crypto.randomBytes(4).toString('hex'));
};

jest.setTimeout(6000000);

it('generated using private registory', async () => {
const outputDir = generateFolderName();
const generator = new Generator('@asyncapi/html-template', outputDir,
{
debug: true,
install: true,
forceWrite: true,
templateParams: {
singleFile: true
},
registry: {
url: 'http://127.0.0.1:4873', // Replace the host.docker.internal to localhost for testing without docker
auth: 'YWRtaW46bmltZGE=' // base64 encoded username and password represented as admin:nimda

}
});
try {
await generator.generateFromFile(dummySpecPath);
// Code to run if the method call is successful
} catch (error) {
// Code to handle the error
console.error('An error occurred:', error);
}

const file = await readFile(path.join(outputDir, 'index.html'), 'utf8');
expect(file).toContain('Dummy example with all spec features included');
});
});

0 comments on commit e95321d

Please sign in to comment.