1 | const crypto = require('crypto');
|
2 |
|
3 | const tryRequire = require('try-require');
|
4 |
|
5 |
|
6 | const React = require('react');
|
7 | const {renderToStaticMarkup} = require('react-dom/server');
|
8 | const {default: convert} = require('@svgr/core');
|
9 |
|
10 |
|
11 | const polarisIcons = tryRequire('@shopify/polaris-icons') || {};
|
12 |
|
13 | function audit({filenames, contentPerFilename}) {
|
14 | const polarisIconsComponentsPerFilename = Object.entries(polarisIcons).reduce(
|
15 | (memo, [importKey, component]) => {
|
16 | memo[`@shopify/polaris-icons/${importKey}.svg`] = component;
|
17 | return memo;
|
18 | },
|
19 | {},
|
20 | );
|
21 | filenames.unshift(...Object.keys(polarisIconsComponentsPerFilename));
|
22 |
|
23 | const contentsPerFilename = filenames.map((filename) => {
|
24 | const reactComponent = filename.startsWith('@shopify/polaris-icons')
|
25 | ? polarisIconsComponentsPerFilename[filename]
|
26 | : componentFromSvgFile(filename, contentPerFilename);
|
27 | return renderToStaticMarkup(reactComponent());
|
28 | });
|
29 |
|
30 | const dependentsByHash = filenames.reduce((memo, filename, i) => {
|
31 | const fileContents = contentsPerFilename[i];
|
32 | const contentHash = md5String(JSON.stringify(fileContents));
|
33 |
|
34 |
|
35 | if (!memo.hasOwnProperty(contentHash)) {
|
36 | memo[contentHash] = [];
|
37 | }
|
38 | memo[contentHash].push(filename);
|
39 |
|
40 | return memo;
|
41 | }, {});
|
42 |
|
43 | const duplicatedDependentsByHash = Object.entries(dependentsByHash).reduce(
|
44 | (memo, [filename, dependents]) => {
|
45 | if (dependents.length > 1) {
|
46 | memo[filename] = dependents;
|
47 | }
|
48 | return memo;
|
49 | },
|
50 | {},
|
51 | );
|
52 |
|
53 | const duplicatedHashesCount = Object.keys(duplicatedDependentsByHash).length;
|
54 |
|
55 | return {
|
56 | summary: `Found ${duplicatedHashesCount} content hashes shared by multiple files`,
|
57 | status: duplicatedHashesCount > 0 ? 'error' : 'pass',
|
58 | info: Object.entries(duplicatedDependentsByHash)
|
59 | .map(([hash, duplicatedDependents]) => {
|
60 | const count = duplicatedDependents.length;
|
61 |
|
62 | const filesStr = duplicatedDependents
|
63 | .map((file) => ` ${file}`)
|
64 | .join('\n');
|
65 |
|
66 | return ` ${hash} matches content used in ${count} files:\n${filesStr}`;
|
67 | })
|
68 | .join('\n'),
|
69 | };
|
70 | }
|
71 |
|
72 | function md5String(string) {
|
73 | return crypto.createHash('md5').update(string).digest('hex');
|
74 | }
|
75 |
|
76 | function componentFromSvgFile(filename, contentPerFilename) {
|
77 | const source = contentPerFilename[filename];
|
78 |
|
79 | const svgrOutput = convert.sync(
|
80 | source,
|
81 | {
|
82 | plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'],
|
83 | svgoConfig: {
|
84 | plugins: [{removeTitle: true}, {removeViewBox: false}],
|
85 | },
|
86 | replaceAttrValues: {
|
87 | '#5C5F62': '{undefined}',
|
88 | '#5c5f62': '{undefined}',
|
89 | },
|
90 | template: ({template}, opts, {jsx}) => template.ast`(props) => ${jsx}`,
|
91 | jsx: {
|
92 | babelConfig: {
|
93 | plugins: [['@babel/plugin-transform-react-jsx']],
|
94 | },
|
95 | },
|
96 | },
|
97 | {
|
98 | filePath: filename,
|
99 | },
|
100 | );
|
101 |
|
102 |
|
103 | return eval(svgrOutput);
|
104 | }
|
105 |
|
106 | audit.auditName = 'duplicate-content';
|
107 | module.exports = audit;
|