Skip to content

Commit

Permalink
Merge pull request #10 from snyk-tech-services/feat/output-pdf
Browse files Browse the repository at this point in the history
feat: refactor to accomodate pdf output
  • Loading branch information
lili2311 authored Oct 2, 2020
2 parents d3bded6 + f4d7d02 commit 5cfd350
Show file tree
Hide file tree
Showing 17 changed files with 607 additions and 156 deletions.
19 changes: 19 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
version: 2.1
orbs:
snyk: snyk/[email protected]
puppeteer: threetreeslight/[email protected]
jobs:
build-test-monitor:
docker:
Expand All @@ -9,6 +10,15 @@ jobs:
- checkout
- run: npm install semantic-release @semantic-release/exec pkg --save-dev
- run: npm install
- run:
name: Install Headless Chrome dependencies
command: |
sudo apt-get install -yq \
gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
- run: npm test
- snyk/scan:
fail-on-issues: true
Expand All @@ -21,6 +31,15 @@ jobs:
steps:
- checkout
- run: npm install
- run:
name: Install Headless Chrome dependencies
command: |
sudo apt-get install -yq \
gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
- run: npm test
- snyk/scan:
fail-on-issues: true
Expand Down
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,29 @@ Ensure `SNYK_TOKEN` is set and has access to the Organization you want to genera
- `json` - generate the raw JSON licenses & dependencies data
- `generate` - generates an HTML report of licenses & dependencies data

Example usage:
`snyk-licenses-report help`
`snyk-licenses-report json --orgPublicId=<ORG_PUBLIC_ID>`
`snyk-licenses-report generate --orgPublicId=<ORG_PUBLIC_ID>`
### Supported Options


Example usage:
- See help:
`snyk-licenses-report help`
- Get JSON output only:
`snyk-licenses-report json --orgPublicId=<ORG_PUBLIC_ID>`
- Default HTML report (Licenses per Org view):
`snyk-licenses-report generate --orgPublicId=<ORG_PUBLIC_ID>`
- PDF report (Licenses per Org view):
`snyk-licenses-report generate --orgPublicId=<ORG_PUBLIC_ID> --outputFormat=pdf`
- Custom Handlebars.js template provided:
`snyk-licenses-report generate --orgPublicId=<ORG_PUBLIC_ID> --outputFormat=pdf --template="PATH/TO/TEMPLATE/template.hsb"`
The data in the template is available is in the format:
```
{
licenses: LicenseReportData;
orgPublicId: string;
orgData: OrgData;
}
```
See the relevant TypeScript types in the repo for full information.

## Development setup
- `npm i`
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"handlebars": "4.7.6",
"lodash": "4.17.20",
"node-fetch": "2.6.1",
"puppeteer": "5.3.1",
"snyk-api-ts-client": "1.5.2",
"snyk-config": "^3.0.0",
"source-map-support": "^0.5.16",
Expand Down
46 changes: 26 additions & 20 deletions src/cmds/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,21 @@ import {
LicenseReportData,
generateLicenseData,
} from '../lib/generate-org-license-report';
import {
generateHtmlReport,
generatePdfReport,
SupportedViews,
} from '../lib/generate-output';
import { writeContentsToFile } from '../lib/write-contents-to-file';
import { generateHtmlReport } from '../lib/generate-report';
import { getOrgData } from '../lib/get-org-data';
import { generateReportName } from '../lib/generate-report-name';
import { SupportedViews } from '../lib/types';
import { saveHtmlReport, savePdfReport } from '../lib/generate-output';
const debug = debugLib('snyk-licenses:generate');

const outputHandlers = {
[OutputFormat.HTML]: generateHtmlReport,
// [OutputFormat.PDF]: generatePdfReport
[OutputFormat.HTML]: saveHtmlReport,
[OutputFormat.PDF]: savePdfReport,
};

const enum OutputFormat {
HTML = 'html',
// TODO: support later
// PDF = 'pdf',
PDF = 'pdf',
}

export const desc =
Expand All @@ -39,8 +38,7 @@ export const builder = {
outputFormat: {
default: OutputFormat.HTML,
desc: 'Report format',
// TODO: add also PDF when ready
options: [OutputFormat.HTML],
choices: [OutputFormat.HTML, OutputFormat.PDF],
},
view: {
// TODO: add also dependency based view when ready
Expand All @@ -67,15 +65,23 @@ export async function handler(argv: {
const licenseData: LicenseReportData = await generateLicenseData(
orgPublicId,
);
const orgData = await getOrgData(orgPublicId);
const reportData = await generateHtmlReport(
orgPublicId,
licenseData,
orgData,
template,
view,
);
const generateReportFunc = outputHandlers[outputFormat];
const res = await generateReportFunc(orgPublicId, licenseData, template, view);
if (res) {
const outputFileName = `${orgPublicId}-${view}.html`;
const outputFile = pathLib.resolve(__dirname, outputFileName);
debug(`ℹ️ Saving generated report to ${outputFile}`);
writeContentsToFile(res, outputFile);
console.log('License report saved at ' + outputFile);
}
const reportFileName = `${generateReportName(
orgData,
view,
)}.${outputFormat}`;
await generateReportFunc(reportFileName, reportData);
console.log(
`${outputFormat.toUpperCase()} license report saved at: ${pathLib.resolve(__dirname, reportFileName)}`,
);
} catch (e) {
console.error(e);
}
Expand Down
2 changes: 1 addition & 1 deletion src/cmds/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as debugLib from 'debug';

import { getApiToken } from '../lib/get-api-token';
import { generateLicenseData } from '../lib/generate-org-license-report';
import { SupportedViews } from '../lib/generate-output';
import { SupportedViews } from '../lib/types';

const debug = debugLib('snyk-licenses:json');

Expand Down
113 changes: 13 additions & 100 deletions src/lib/generate-output/html/index.ts
Original file line number Diff line number Diff line change
@@ -1,107 +1,20 @@
import * as Handlebars from 'handlebars';
import * as path from 'path';
import * as pathLib from 'path';
import * as fs from 'fs';
import * as debugLib from 'debug';
import { writeContentsToFile } from '../../write-contents-to-file';

import { LicenseReportData } from '../../generate-org-license-report';
import { getOrgData, OrgData } from '../../get-org-data';
const debug = debugLib('snyk-licenses:saveHtmlReport');

const debug = debugLib('snyk-licenses:generateHtmlReport');
const DEFAULT_TEMPLATE = './templates/licenses-view.hbs';

export const enum SupportedViews {
ORG_LICENSES = 'org-licenses',
// TODO: support later
// PROJECT_DEPENDENCIES = 'project-dependencies',
}

const transformDataFunc = {
[SupportedViews.ORG_LICENSES]: transformDataForLicenseView,
// TODO: support later
// [SupportedViews.PROJECT_DEPENDENCIES]: transformDataForDependencyView,
};

export async function generateHtmlReport(
orgPublicId: string,
data: LicenseReportData,
templateOverridePath: string | undefined = undefined,
view: SupportedViews = SupportedViews.ORG_LICENSES,
) {
// TODO: add any helpers & data transformations that are useful here
debug('ℹ️ Generating HTML report');
const hbsTemplate = selectTemplate(view, templateOverridePath);
debug(
`✅ Using template ${
hbsTemplate === DEFAULT_TEMPLATE ? 'default template' : hbsTemplate
}`,
);
debug(`✅ Registered Handlebars.js partials`);
const htmlTemplate = await compileTemplate(hbsTemplate);
debug(`✅ Compiled template ${hbsTemplate}`);

const orgData = await getOrgData(orgPublicId);
const transformedData = transformDataFunc[view](orgPublicId, data, orgData);

return htmlTemplate(transformedData);
}

function transformDataForLicenseView(
orgPublicId: string,
data: LicenseReportData,
orgData: OrgData,
): {
licenses: LicenseReportData;
orgPublicId: string;
orgData: OrgData;
} {
return { licenses: data, orgPublicId, orgData };
}

// TODO: support later
// function transformDataForDependencyView(data: LicenseReportData) {
// return data;
// }

function selectTemplate(view: SupportedViews, templateOverride?): string {
switch (view) {
case SupportedViews.ORG_LICENSES:
return templateOverride || DEFAULT_TEMPLATE;
// TODO: support later
// case SupportedViews.PROJECT_DEPENDENCIES:
// return templateOverride || '../templates/project-dependencies-view.hbs';
default:
return DEFAULT_TEMPLATE;
}
}

async function registerPeerPartial(
templatePath: string,
name: string,
): Promise<void> {
const file = path.join(__dirname, templatePath);
debug(`ℹ️ Registering peer partial template ${file}`);

const template = await compileTemplate(file);
debug(`✅ Compiled template ${file}`);

Handlebars.registerPartial(name, template);
}

async function compileTemplate(
export async function saveHtmlReport(
fileName: string,
): Promise<HandlebarsTemplateDelegate> {
return readFile(path.resolve(__dirname, fileName), 'utf8').then(
Handlebars.compile,
);
}

function readFile(filePath: string, encoding: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
fs.readFile(filePath, encoding, (err, data) => {
if (err) {
reject(err);
}
resolve(data);
});
});
data: string,
): Promise<void> {
if (!data) {
throw new Error('No report data to save!');
}
const outputFile = pathLib.resolve(__dirname, fileName);
debug(`⏳ Saving generated report to ${outputFile}`);
writeContentsToFile(data, outputFile);
debug(`✅ Saved HTML report to ${outputFile}`);
}
25 changes: 21 additions & 4 deletions src/lib/generate-output/pdf/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
export function generatePdfReport() {
// TODO: find a package that could convert the HTML to pdf
// or where we could use the same handlebars template & css
// to generate the pdf
import * as puppeteer from 'puppeteer';
import * as debugLib from 'debug';

const debug = debugLib('snyk-licenses:generatePdfReport');

export async function savePdfReport(
fileName: string,
data: string,
): Promise<void> {
if (!data) {
throw new Error('No report data to save!');
}
debug(`⏳ Saving PDF to ${fileName}`);
// start browser in headless mode
const browser = await puppeteer.launch();
const page = await browser.newPage();
// We set the page content as the generated html by handlebars
await page.setContent(data);
await page.pdf({ path: fileName, format: 'A4' });
await browser.close();
debug(`✅ Saved PDF report to ${fileName}`);
}
5 changes: 5 additions & 0 deletions src/lib/generate-report-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { OrgData } from "./get-org-data";

export function generateReportName(orgData: OrgData, view: string): string {
return `${orgData.slug}-${orgData.id}-${view}`;
}
Loading

0 comments on commit 5cfd350

Please sign in to comment.