1 | const fs = require('fs-extra');
|
2 | const path = require('path');
|
3 | const os = require('os');
|
4 | const { promisify } = require('util');
|
5 |
|
6 | const request = require('request-promise-native');
|
7 | const extract = promisify(require('extract-zip'));
|
8 |
|
9 | const { logger } = require('./logger');
|
10 | const {
|
11 | logFileSystemErrorInstance,
|
12 | logErrorInstance,
|
13 | } = require('./errorHandlers');
|
14 |
|
15 |
|
16 | const USER_AGENT_HEADERS = { 'User-Agent': 'HubSpot/hubspot-cms-tools' };
|
17 | const TMP_BOILERPLATE_FOLDER_PREFIX = 'hubspot-cms-theme-boilerplate-';
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | async 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 |
|
49 |
|
50 |
|
51 | async 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 |
|
72 |
|
73 |
|
74 | async function extractThemeZip(zip) {
|
75 | logger.log('Extracting theme source...');
|
76 |
|
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 |
|
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 |
|
117 |
|
118 |
|
119 |
|
120 | async 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 |
|
141 |
|
142 |
|
143 | function cleanupTemp(tmpDir) {
|
144 | if (!tmpDir) return;
|
145 | try {
|
146 | fs.remove(tmpDir);
|
147 | } catch (e) {
|
148 |
|
149 | }
|
150 | }
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 | async 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 |
|
172 | module.exports = {
|
173 | createTheme,
|
174 | downloadCmsThemeBoilerplate,
|
175 | extractThemeZip,
|
176 | fetchReleaseData,
|
177 | };
|