UNPKG

5.07 kBJavaScriptView Raw
1const fs = require('fs-extra');
2const path = require('path');
3const os = require('os');
4const { promisify } = require('util');
5
6const request = require('request-promise-native');
7const extract = promisify(require('extract-zip'));
8
9const { logger } = require('./logger');
10const {
11 logFileSystemErrorInstance,
12 logErrorInstance,
13} = require('./errorHandlers');
14
15// https://developer.github.com/v3/#user-agent-required
16const USER_AGENT_HEADERS = { 'User-Agent': 'HubSpot/hubspot-cms-tools' };
17const TMP_BOILERPLATE_FOLDER_PREFIX = 'hubspot-cms-theme-boilerplate-';
18
19/**
20 * https://developer.github.com/v3/repos/releases/#get-the-latest-release
21 * @param {String} tag - Git tag to fetch for. If omitted latest will be fetched.
22 */
23async function fetchReleaseData(tag = '') {
24 tag = tag.trim().toLowerCase();
25 if (tag.length && tag[0] !== 'v') {
26 tag = `v${tag}`;
27 }
28 const URI = tag
29 ? `https://api.github.com/repos/HubSpot/cms-theme-boilerplate/releases/tags/${tag}`
30 : 'https://api.github.com/repos/HubSpot/cms-theme-boilerplate/releases/latest';
31 try {
32 return await request.get(URI, {
33 headers: { ...USER_AGENT_HEADERS },
34 json: true,
35 });
36 } catch (err) {
37 logger.error(
38 `Failed fetching release data for ${tag || 'latest'} theme boilerplate.`
39 );
40 if (tag && err.statusCode === 404) {
41 logger.error(`Theme boilerplate ${tag} not found.`);
42 }
43 }
44 return null;
45}
46
47/**
48 * @param {String} tag - Git tag to fetch for. If omitted latest will be fetched.
49 * @returns {Buffer|Null} Zip data buffer
50 */
51async function downloadCmsThemeBoilerplate(tag = '') {
52 try {
53 const releaseData = await fetchReleaseData(tag);
54 if (!releaseData) return;
55 const { zipball_url: zipUrl, name } = releaseData;
56 logger.log(`Fetching ${name}...`);
57 const zip = await request.get(zipUrl, {
58 encoding: null,
59 headers: { ...USER_AGENT_HEADERS },
60 });
61 logger.log('Completed theme fetch.');
62 return zip;
63 } catch (err) {
64 logger.error('An error occured fetching the theme source.');
65 logErrorInstance(err);
66 }
67 return null;
68}
69
70/**
71 * @param {Buffer} zip
72 * @returns {String|Null} Temp dir where zip has been extracted.
73 */
74async function extractThemeZip(zip) {
75 logger.log('Extracting theme source...');
76 // Write zip to disk
77 let tmpDir;
78 let tmpZipPath;
79 try {
80 tmpDir = await fs.mkdtemp(
81 path.join(os.tmpdir(), TMP_BOILERPLATE_FOLDER_PREFIX)
82 );
83 tmpZipPath = path.join(tmpDir, 'boilerplate.zip');
84 await fs.ensureFile(tmpZipPath);
85 await fs.writeFile(tmpZipPath, zip, {
86 mode: 0o777,
87 });
88 } catch (err) {
89 logger.error('An error occured writing temp theme source.');
90 if (tmpZipPath || tmpDir) {
91 logFileSystemErrorInstance(err, {
92 filepath: tmpZipPath || tmpDir,
93 write: true,
94 });
95 } else {
96 logErrorInstance(err);
97 }
98 return null;
99 }
100 // Extract zip
101 let extractDir = null;
102 try {
103 const tmpExtractPath = path.join(tmpDir, 'extracted');
104 await extract(tmpZipPath, { dir: tmpExtractPath });
105 extractDir = tmpExtractPath;
106 } catch (err) {
107 logger.error('An error occured extracting theme source.');
108 logErrorInstance(err);
109 return null;
110 }
111 logger.log('Completed theme source extraction.');
112 return { extractDir, tmpDir };
113}
114
115/**
116 * @param {String} src - Dir where boilerplate repo files have been extracted.
117 * @param {String} dest - Dir to copy boilerplate src files to.
118 * @returns {Boolean} `true` if successfully copied, `false` otherwise.
119 */
120async function copyThemeBoilerplateToDest(src, dest) {
121 try {
122 logger.log('Copying theme source...');
123 const files = await fs.readdir(src);
124 const rootDir = files[0];
125 const themeSrcDir = path.join(src, rootDir, 'src');
126 await fs.copy(themeSrcDir, dest);
127 logger.log('Completed copying theme source.');
128 return true;
129 } catch (err) {
130 logger.error(`An error occured copying theme source to ${dest}.`);
131 logFileSystemErrorInstance(err, {
132 filepath: dest,
133 write: true,
134 });
135 }
136 return false;
137}
138
139/**
140 * Try cleaning up resources from os's tempdir
141 * @param {String} tmpDir
142 */
143function cleanupTemp(tmpDir) {
144 if (!tmpDir) return;
145 try {
146 fs.remove(tmpDir);
147 } catch (e) {
148 // noop
149 }
150}
151
152/**
153 * Writes a copy of the boilerplate theme to dest.
154 * @param {String} dest - Dir top write theme src to.
155 * @param {Object} options
156 * @returns {Boolean} `true` if successful, `false` otherwise.
157 */
158async function createTheme(dest, type, options = {}) {
159 const { themeVersion: tag } = options;
160 const zip = await downloadCmsThemeBoilerplate(tag);
161 if (!zip) return false;
162 const { extractDir, tmpDir } = (await extractThemeZip(zip)) || {};
163 const success =
164 extractDir != null && (await copyThemeBoilerplateToDest(extractDir, dest));
165 if (success) {
166 logger.log(`Success: your new ${type} project has been created in ${dest}`);
167 }
168 cleanupTemp(tmpDir);
169 return success;
170}
171
172module.exports = {
173 createTheme,
174 downloadCmsThemeBoilerplate,
175 extractThemeZip,
176 fetchReleaseData,
177};