Skip to content

Commit bd9b8d0

Browse files
authored
Visual Package Diff (#96)
* add basic package diff * add basic styling and labels * basic working version * tidy up * history, readme, test placeholder * Update index.test.js comment out require * Update index.test.js
1 parent 3bbdaeb commit bd9b8d0

File tree

7 files changed

+367
-0
lines changed

7 files changed

+367
-0
lines changed

packages/util-package-diff/HISTORY.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# History
2+
3+
## 0.0.1 (2022-10-20)
4+
* Initial commit
5+
* Experimental version, no tests yet

packages/util-package-diff/README.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Visual Package Diff
2+
3+
[![NPM version][badge-npm]][info-npm]
4+
[![Node version][badge-node]][info-node]
5+
![MIT License][badge-license]
6+
7+
Visually compare two versions of an Elements component package. Pass the name and version of a package and the demo is retrieved from NPM. A local server is created and the NPM package demo is displayed side-by-side with the current local demo of the component package.
8+
9+
Can be used to test the affect of `brand-context` or other updates on a component package.
10+
11+
## Usage
12+
13+
```
14+
$ npx sn-package-diff --help
15+
16+
visual diff between two elements packages on a local server
17+
18+
Usage
19+
sn-package-diff [options]
20+
21+
Options
22+
--package, -p Name and version of package
23+
--scope, -s NPM scope, default: @springernature
24+
--port, -t Port for local server, default: 3000
25+
26+
Examples
27+
sn-package-diff -p [email protected]
28+
sn-package-diff -p [email protected] -s @some-other-scope
29+
sn-package-diff -p [email protected] -s @some-other-scope -t 5000
30+
```
31+
32+
### Example
33+
34+
`$ npx sn-package-diff -p [email protected] -t 5000`
35+
36+
## License
37+
38+
[MIT License][info-license] © 2019, Springer Nature
39+
40+
[info-npm]: https://www.npmjs.com/package/@springernature/util-package-diff
41+
[badge-npm]: https://img.shields.io/npm/v/@springernature/util-package-diff.svg
42+
[info-license]: https://github.com/springernature/frontend-toolkit-utilities/blob/master/LICENCE
43+
[badge-license]: https://img.shields.io/badge/license-MIT-blue.svg
44+
[badge-node]: https://img.shields.io/badge/node->=16-brightgreen.svg
45+
[info-node]: package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* __tests__/unit/lib/index.test.js
3+
* Test: lib/index.js
4+
*/
5+
'use strict';
6+
7+
jest.mock('@springernature/util-cli-reporter');
8+
9+
// const diffPackage = require('../../../lib');
10+
11+
describe('package diff', () => {
12+
test('diff two packages', () => {
13+
// await diffPackage('[email protected]', '@springernature', 3000);
14+
expect(1 + 1).toBe(2);
15+
});
16+
});

packages/util-package-diff/bin/cli.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#! /usr/bin/env node
2+
'use strict';
3+
4+
const meow = require('meow');
5+
const PrettyError = require('pretty-error');
6+
7+
const diffPackage = require('../lib');
8+
9+
const pe = new PrettyError();
10+
11+
const cli = meow(`
12+
Usage
13+
sn-package-diff [options]
14+
15+
Options
16+
--package, -p Name and version of package
17+
--scope, -s NPM scope, default: @springernature
18+
--port, -t Port for local server, default: 3000
19+
20+
Examples
21+
sn-package-diff -p [email protected]
22+
sn-package-diff -p [email protected] -s @some-other-scope
23+
sn-package-diff -p [email protected] -s @some-other-scope -t 5000
24+
`, {
25+
booleanDefault: undefined,
26+
flags: {
27+
package: {
28+
type: 'string',
29+
alias: 'p'
30+
},
31+
scope: {
32+
type: 'string',
33+
alias: 's',
34+
default: '@springernature'
35+
},
36+
port: {
37+
type: 'number',
38+
alias: 't',
39+
default: 3000
40+
}
41+
}
42+
});
43+
44+
(async () => {
45+
await diffPackage(cli.flags.package, cli.flags.scope, cli.flags.port);
46+
})().catch(error => {
47+
const renderedError = pe.render(error);
48+
console.log(renderedError);
49+
process.exit(1);
50+
});
+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
'use strict';
2+
3+
const fs = require('fs').promises;
4+
const path = require('path');
5+
const http = require('http');
6+
const globby = require('globby');
7+
const got = require('got');
8+
const htmlminifier = require('html-minifier').minify;
9+
const reporter = require('@springernature/util-cli-reporter');
10+
const baseTemplate = require('./template');
11+
12+
// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
13+
const semverRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
14+
const packageCdnLocation = 'https://cdn.jsdelivr.net/npm/';
15+
const packageDemoPath = 'demo/dist/index.html';
16+
const minifyOptions = {
17+
removeAttributeQuotes: true,
18+
removeComments: true,
19+
collapseInlineTagWhitespace: true,
20+
caseSensitive: true,
21+
minifyCSS: true,
22+
minifyJS: true
23+
};
24+
25+
/**
26+
* Check the remote package version number is valid semver
27+
* @private
28+
* @function checkPackageVersion
29+
* @param {String} packageName name of the package
30+
* @param {String} packageVersion version number to check
31+
*/
32+
const checkPackageVersion = (packageName, packageVersion) => {
33+
if (!semverRegex.test(packageVersion)) {
34+
reporter.fail('getting package from npm', `${packageName} v${packageVersion}`, 'invalid version number');
35+
throw new Error(`Invalid semver '${packageVersion}'`);
36+
}
37+
};
38+
39+
/**
40+
* Get the html for displaying the local demo version
41+
* @async
42+
* @private
43+
* @function getLocalPackageHtml
44+
* @param {String} packageName name and version of package on NPM
45+
* @return {Promise<Object>}
46+
*/
47+
const getLocalPackageHtml = async packageName => {
48+
const localDemoFiles = await globby(`**/${packageName}/${packageDemoPath}`, {
49+
expandDirectories: false,
50+
onlyFiles: true
51+
});
52+
53+
if (localDemoFiles.length === 0) {
54+
reporter.fail('unable to find local package demo', packageName);
55+
throw new Error('404: local package demo not found');
56+
}
57+
58+
// Should only be a single rendered demo
59+
// Return the html and version number
60+
const pathToLocalDemo = localDemoFiles[0];
61+
const pathToPackageJson = path.join(pathToLocalDemo.replace(packageDemoPath, ''), 'package.json');
62+
const version = require(path.resolve(pathToPackageJson)).version;
63+
const html = await fs.readFile(pathToLocalDemo, 'utf-8');
64+
65+
return {
66+
html: html,
67+
version: version
68+
};
69+
};
70+
71+
/**
72+
* Create a server and display demos side by side
73+
* @async
74+
* @private
75+
* @function createServer
76+
* @param {String} port port to open local server on
77+
* @param {String} packageName name of the package to display
78+
* @param {Object} remote html and version from npm demo
79+
* @param {Object} local html and version from local demo
80+
* @return {Promise}
81+
*/
82+
const createServer = (port, packageName, remote, local) => {
83+
return new Promise(function (resolve, reject) {
84+
const server = http.createServer();
85+
const remoteHtmlMinified = htmlminifier(remote.html, minifyOptions);
86+
const localHtmlMinified = htmlminifier(local.html, minifyOptions);
87+
const page = baseTemplate(packageName, {
88+
html: remoteHtmlMinified,
89+
version: remote.version
90+
}, {
91+
html: localHtmlMinified,
92+
version: local.version
93+
});
94+
95+
server.on('request', (_req, res) => {
96+
res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
97+
res.end(page);
98+
}).listen(port, () => {
99+
reporter.info('manual diff available', `http://localhost:${port}/`);
100+
resolve();
101+
});
102+
103+
server.on('error', error => {
104+
reporter.fail('unable to create server', `http://localhost:${port}/`);
105+
reject(error);
106+
});
107+
});
108+
};
109+
110+
/**
111+
* Compare local version of package with one from NPM
112+
* @async
113+
* @function diffPackage
114+
* @param {String} npmPackage name and version of package on NPM
115+
* @param {String} scope NPM scope
116+
* @param {Number} port local server port
117+
* @return {Promise}
118+
*/
119+
const diffPackage = async (npmPackage, scope, port) => {
120+
const npmPackageArray = npmPackage.split('@');
121+
const packageName = npmPackageArray[0];
122+
const packageVersion = npmPackageArray[1];
123+
const npmPackageUrl = path.join(packageCdnLocation, `${scope}/${npmPackage}`, packageDemoPath);
124+
let npmPackageHtml;
125+
let localPackage;
126+
127+
reporter.info('visual comparison for package', packageName);
128+
129+
// Check valid semver convention
130+
checkPackageVersion(packageName, packageVersion);
131+
132+
// Get remote package html
133+
try {
134+
npmPackageHtml = await got(npmPackageUrl);
135+
} catch (error) {
136+
reporter.fail('getting package from npm', npmPackage, 'invalid get request');
137+
throw error;
138+
}
139+
140+
// Get local package html and version
141+
try {
142+
localPackage = await getLocalPackageHtml(packageName);
143+
} catch (error) {
144+
reporter.fail('getting package from local', packageName, 'could not find package html');
145+
throw error;
146+
}
147+
148+
// Create server for local comparison
149+
await createServer(port, packageName, {
150+
html: npmPackageHtml.body,
151+
version: packageVersion
152+
}, {
153+
html: localPackage.html,
154+
version: localPackage.version
155+
});
156+
};
157+
158+
module.exports = diffPackage;
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// HTML template wrapper
2+
module.exports = (packageName, remote, local) => {
3+
return `<!doctype html>
4+
<html lang="en">
5+
<head>
6+
<script>
7+
(function(e){var t=e.documentElement,n=e.implementation;t.className='js';})(document)
8+
</script>
9+
<meta charset="utf-8">
10+
<meta name="viewport" content="width=device-width, initial-scale=1">
11+
<title>Visual Diff for ${packageName}</title>
12+
<style>
13+
html,
14+
body {
15+
height: 100%;
16+
margin: 0;
17+
box-sizing: border-box;
18+
}
19+
body {
20+
padding: 2%;
21+
}
22+
html {
23+
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
24+
}
25+
h2 {
26+
margin: 0 0 1em 0;
27+
padding: 0 2%;
28+
}
29+
.column-container {
30+
height: 100%;
31+
min-height: 100%;
32+
display: flex;
33+
flex-direction: column;
34+
}
35+
.column-child {
36+
flex: 1;
37+
}
38+
.row-container {
39+
display: flex;
40+
height: 100%;
41+
}
42+
.row-child {
43+
flex: 1;
44+
}
45+
</style>
46+
</head>
47+
<body>
48+
<div class="column-container">
49+
<div class="column-child">
50+
<div class="row-container">
51+
<div class="row-child">
52+
<h2>${packageName} v${remote.version}</h2>
53+
<iframe height="100%" width="100%" title="${packageName}@${remote.version}" scrolling="auto" allowtransparency="true" loading="lazy" srcdoc='${remote.html}' frameborder="0"></iframe>
54+
</div>
55+
<div class="row-child">
56+
<h2>${packageName} v${local.version}</h2>
57+
<iframe height="100%" width="100%" title="${packageName}@${local.version}" scrolling="auto" allowtransparency="true" loading="lazy" srcdoc='${local.html}' frameborder="0"></iframe>
58+
</div>
59+
</div>
60+
</div>
61+
</div>
62+
</body>
63+
</html>`;
64+
};
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "@springernature/util-package-diff",
3+
"version": "0.0.1",
4+
"description": "visual diff between two elements packages on a local server",
5+
"main": "lib/index.js",
6+
"bin": {
7+
"sn-package-diff": "bin/cli.js"
8+
},
9+
"scripts": {
10+
"test": "echo \"Run tests from the top of the repo please.\" && exit 1"
11+
},
12+
"keywords": [
13+
"elements",
14+
"components",
15+
"diff",
16+
"testing"
17+
],
18+
"author": "ajk",
19+
"license": "MIT",
20+
"dependencies": {
21+
"@springernature/util-cli-reporter": "^2.0.0",
22+
"globby": "^11.0.3",
23+
"got": "^11.8.3",
24+
"html-minifier": "^4.0.0",
25+
"meow": "^9.0.0",
26+
"pretty-error": "^4.0.0",
27+
"server": "^1.0.37"
28+
}
29+
}

0 commit comments

Comments
 (0)