1 | 'use strict';
|
2 |
|
3 | const util = require('util');
|
4 | const path = require('path');
|
5 | const fs = require('fs');
|
6 | const os = require('os');
|
7 | const execSync = require('child_process').execSync;
|
8 |
|
9 | const httpx = require('httpx');
|
10 | const kitx = require('kitx');
|
11 |
|
12 | const exists = util.promisify(fs.exists);
|
13 | const writeFile = util.promisify(fs.writeFile);
|
14 | const readFile = util.promisify(fs.readFile);
|
15 |
|
16 | const debug = require('debug')('fun:deps');
|
17 | const archiver = require('archiver');
|
18 |
|
19 | function read(readable, encoding) {
|
20 | return new Promise((resolve, reject) => {
|
21 | var onData, onError, onEnd;
|
22 |
|
23 | var cleanup = function () {
|
24 |
|
25 | readable.removeListener('error', onError);
|
26 | readable.removeListener('data', onData);
|
27 | readable.removeListener('end', onEnd);
|
28 | };
|
29 |
|
30 | const bufs = [];
|
31 | var size = 0;
|
32 |
|
33 | onData = function (buf) {
|
34 | bufs.push(buf);
|
35 | size += buf.length;
|
36 | };
|
37 |
|
38 | onError = function (err) {
|
39 | cleanup();
|
40 | reject(err);
|
41 | };
|
42 |
|
43 | onEnd = function () {
|
44 | cleanup();
|
45 | var buff = Buffer.concat(bufs, size);
|
46 |
|
47 | if (encoding) {
|
48 | const result = buff.toString(encoding);
|
49 | return resolve(result);
|
50 | }
|
51 |
|
52 | resolve(buff);
|
53 | };
|
54 |
|
55 | readable.on('error', onError);
|
56 | readable.on('data', onData);
|
57 | readable.on('end', onEnd);
|
58 | });
|
59 | }
|
60 |
|
61 | const zip = function (rootDir) {
|
62 | const archive = archiver('zip', {
|
63 | zlib: { level: 9 }
|
64 | });
|
65 |
|
66 |
|
67 | archive.on('warning', function(err) {
|
68 | console.log(err);
|
69 | if (err.code === 'ENOENT') {
|
70 |
|
71 | } else {
|
72 |
|
73 | throw err;
|
74 | }
|
75 | });
|
76 |
|
77 | archive.on('error', (e) => console.log(e));
|
78 |
|
79 | if (debug.enabled) {
|
80 | archive.on('entry', function (entry) {
|
81 | debug('entry: %j', entry);
|
82 | });
|
83 |
|
84 | archive.on('progress', (p) => {
|
85 | console.log('progress');
|
86 | debug('progress: %j', p);
|
87 | });
|
88 | }
|
89 |
|
90 | archive.glob('node_modules/**', {
|
91 | cwd: rootDir
|
92 | });
|
93 | archive.finalize();
|
94 |
|
95 | return read(archive, 'base64');
|
96 | };
|
97 |
|
98 | function pkg(deps) {
|
99 | return `{
|
100 | "dependencies": ${JSON.stringify(deps)}
|
101 | }`;
|
102 | }
|
103 |
|
104 | async function localBuild(data) {
|
105 | const runtime = data.runtime;
|
106 | const deps = pkg(data.dependencies);
|
107 | const digest = kitx.md5(`${runtime}:${deps}`, 'hex');
|
108 | var dir = `${os.tmpdir()}/${digest}`;
|
109 | try {
|
110 | fs.mkdirSync(dir);
|
111 | } catch (ex) {
|
112 |
|
113 | }
|
114 |
|
115 | const pkgPath = path.join(dir, 'package.json');
|
116 | fs.writeFileSync(pkgPath, deps);
|
117 | execSync('npm i --registry=https://registry.npm.taobao.org', {
|
118 | cwd: dir
|
119 | });
|
120 |
|
121 | return zip(dir);
|
122 | }
|
123 |
|
124 | async function remoteBuild(data) {
|
125 | const url = `http://59766a59fa0b4391b703b51d97230296-cn-hangzhou.alicloudapi.com/build/${data.runtime}`;
|
126 |
|
127 | const response = await httpx.request(url, {
|
128 | method: 'POST',
|
129 | timeout: 60000,
|
130 | headers: {
|
131 | 'content-type': 'application/json'
|
132 | },
|
133 | data: JSON.stringify(data)
|
134 | });
|
135 |
|
136 | debug('%j', data);
|
137 |
|
138 | const statusCode = response.statusCode;
|
139 |
|
140 | var body = await httpx.read(response, 'utf8');
|
141 | const headers = response.headers;
|
142 | const contentType = headers['content-type'] || '';
|
143 |
|
144 | if (contentType.startsWith('application/json')) {
|
145 | body = JSON.parse(body);
|
146 | }
|
147 |
|
148 | if (statusCode !== 200) {
|
149 | debug(response.headers);
|
150 | debug('statusCode: %s', statusCode);
|
151 | debug('build dependencies failed!');
|
152 | let err = new Error(`${headers['x-ca-error-message']},` +
|
153 | ` requestid: ${headers['x-ca-request-id']}`);
|
154 | err.name = 'BuildError';
|
155 | err.data = data;
|
156 | throw err;
|
157 | }
|
158 |
|
159 | if (!body.ok) {
|
160 | let err = new Error(`Build failed, ${body.message}`);
|
161 | err.name = 'BuildError';
|
162 | err.data = data;
|
163 | throw err;
|
164 | }
|
165 |
|
166 | return body.data.zip;
|
167 | }
|
168 |
|
169 | async function buildDeps(func, rootDir, type) {
|
170 | const runtime = (func.runtime || 'nodejs4.4').replace('.', '_');
|
171 |
|
172 | const buildType = type || process.env.BUILD_TYPE || 'remote';
|
173 | if (buildType !== 'local' && buildType !== 'remote') {
|
174 | throw new TypeError(`BUILD_TYPE must be 'local' or 'remote'.`);
|
175 | }
|
176 |
|
177 |
|
178 | const pkgPath = path.join(rootDir, 'package.json');
|
179 | const hasPackageFile = await exists(pkgPath);
|
180 | if (!hasPackageFile) {
|
181 | debug('The package.json inexists in Project, skipped.');
|
182 | return;
|
183 | }
|
184 |
|
185 | const pkg = require(pkgPath);
|
186 | const dependencies = pkg.dependencies || {};
|
187 | if (Object.keys(dependencies).length === 0) {
|
188 | debug('The package.json has not any dependencies in Project, skipped.');
|
189 | return;
|
190 | }
|
191 |
|
192 | const data = {
|
193 | runtime: runtime,
|
194 | dependencies: dependencies
|
195 | };
|
196 |
|
197 | const stringToHash = `${runtime}:${JSON.stringify(dependencies)}`;
|
198 |
|
199 | const hash = kitx.md5(stringToHash, 'hex');
|
200 | const zipPath = path.join(rootDir, `node_modules_${hash}.zip`);
|
201 | const md5Path = path.join(rootDir, `node_modules_${hash}.zip.md5`);
|
202 | const hasZip = await exists(zipPath);
|
203 | const hasSign = await exists(md5Path);
|
204 |
|
205 | if (hasZip && hasSign) {
|
206 | const zip = await readFile(zipPath, 'base64');
|
207 | const sign = await readFile(md5Path, 'utf8');
|
208 | if (sign === kitx.md5(zip, 'hex')) {
|
209 | debug('The node_modules pre-compressed, skipped.');
|
210 | return zip;
|
211 | }
|
212 | }
|
213 |
|
214 | var base64;
|
215 |
|
216 | if (buildType === 'remote') {
|
217 | debug('build deps remotely.');
|
218 | base64 = await remoteBuild(data);
|
219 | } else {
|
220 | debug('build deps locally.');
|
221 | base64 = await localBuild(data);
|
222 | }
|
223 |
|
224 | debug('build %j completed.', func);
|
225 | const digest = kitx.md5(base64, 'hex');
|
226 | await writeFile(zipPath, base64, 'base64');
|
227 | await writeFile(md5Path, digest);
|
228 |
|
229 | return base64;
|
230 | }
|
231 |
|
232 | module.exports = { buildDeps, read };
|