@adobe/aem-cli 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5262 -0
- package/CODE_OF_CONDUCT.md +74 -0
- package/CONTRIBUTING.md +64 -0
- package/LICENSE.txt +264 -0
- package/README.md +147 -0
- package/index.js +25 -0
- package/package.json +97 -0
- package/src/abstract-server.cmd.js +143 -0
- package/src/abstract.cmd.js +42 -0
- package/src/cli-util.js +74 -0
- package/src/cli.js +137 -0
- package/src/config/config-utils.js +49 -0
- package/src/fetch-utils.js +65 -0
- package/src/git-utils.js +265 -0
- package/src/hack.cmd.js +48 -0
- package/src/hack.js +50 -0
- package/src/import.cmd.js +103 -0
- package/src/import.js +110 -0
- package/src/log-common.js +132 -0
- package/src/md5.js +34 -0
- package/src/package.cjs +12 -0
- package/src/server/BaseProject.js +137 -0
- package/src/server/BaseServer.js +205 -0
- package/src/server/HeadHtmlSupport.js +180 -0
- package/src/server/HelixImportProject.js +30 -0
- package/src/server/HelixImportServer.js +207 -0
- package/src/server/HelixProject.js +130 -0
- package/src/server/HelixServer.js +120 -0
- package/src/server/Indexer.js +152 -0
- package/src/server/LiveReload.js +276 -0
- package/src/server/RequestContext.js +205 -0
- package/src/server/utils.js +457 -0
- package/src/up.cmd.js +171 -0
- package/src/up.js +120 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2018 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import crypto from 'crypto';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { Socket } from 'net';
|
|
16
|
+
import { PassThrough } from 'stream';
|
|
17
|
+
import cookie from 'cookie';
|
|
18
|
+
import { getFetch } from '../fetch-utils.js';
|
|
19
|
+
|
|
20
|
+
const utils = {
|
|
21
|
+
status2level(status, debug3xx) {
|
|
22
|
+
if (status < 300) {
|
|
23
|
+
return 'debug';
|
|
24
|
+
}
|
|
25
|
+
if (status < 400) {
|
|
26
|
+
return debug3xx ? 'debug' : 'info';
|
|
27
|
+
}
|
|
28
|
+
if (status < 500) {
|
|
29
|
+
return 'warn';
|
|
30
|
+
}
|
|
31
|
+
return 'error';
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Checks if the file addressed by the given filename exists and is a regular file.
|
|
36
|
+
* @param {String} filename Path to file
|
|
37
|
+
* @returns {Promise} Returns promise that resolves with the filename or rejects if is not a file.
|
|
38
|
+
*/
|
|
39
|
+
async isFile(filename) {
|
|
40
|
+
const stats = await fs.stat(filename);
|
|
41
|
+
if (!stats.isFile()) {
|
|
42
|
+
throw Error(`no regular file: ${filename}`);
|
|
43
|
+
}
|
|
44
|
+
return filename;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fetches content from the given uri
|
|
49
|
+
* @param {String} uri URL to fetch
|
|
50
|
+
* @param {RequestContext} ctx the context
|
|
51
|
+
* @param {object} auth authentication object ({@see https://github.com/request/request#http-authentication})
|
|
52
|
+
* @returns {Buffer} The requested content or NULL if not exists.
|
|
53
|
+
*/
|
|
54
|
+
async fetch(ctx, uri, auth) {
|
|
55
|
+
const headers = {
|
|
56
|
+
'X-Request-Id': ctx.requestId,
|
|
57
|
+
};
|
|
58
|
+
if (auth) {
|
|
59
|
+
headers.authorization = `Bearer ${auth}`;
|
|
60
|
+
}
|
|
61
|
+
const res = await getFetch()(uri, {
|
|
62
|
+
cache: 'no-store',
|
|
63
|
+
headers,
|
|
64
|
+
});
|
|
65
|
+
const body = await res.buffer();
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const level = utils.status2level(res.status);
|
|
68
|
+
ctx.log[level](`resource at ${uri} does not exist. got ${res.status} from server`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return body;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Injects the live-reload script
|
|
76
|
+
* @param {string} body the html body
|
|
77
|
+
* @param {HelixServer} server the proxy server
|
|
78
|
+
* @returns {string} the modified body
|
|
79
|
+
*/
|
|
80
|
+
injectLiveReloadScript(body, server) {
|
|
81
|
+
let match = body.match(/<\/head>/i);
|
|
82
|
+
if (!match) {
|
|
83
|
+
match = body.match(/<\/body>/i);
|
|
84
|
+
}
|
|
85
|
+
if (!match) {
|
|
86
|
+
match = body.match(/<\/html>/i);
|
|
87
|
+
}
|
|
88
|
+
// don't inject if no html found at all.
|
|
89
|
+
if (match) {
|
|
90
|
+
const { index } = match;
|
|
91
|
+
// eslint-disable-next-line no-param-reassign
|
|
92
|
+
let newbody = body.substring(0, index);
|
|
93
|
+
if (process.env.CODESPACES === 'true') {
|
|
94
|
+
newbody += `<script>
|
|
95
|
+
window.LiveReloadOptions = {
|
|
96
|
+
host: new URL(location.href).hostname.replace(/-[0-9]+\\.preview\\.app\\.github\\.dev/, '-35729.preview.app.github.dev'),
|
|
97
|
+
port: 443,
|
|
98
|
+
https: true,
|
|
99
|
+
};
|
|
100
|
+
</script>`;
|
|
101
|
+
} else {
|
|
102
|
+
newbody += `<script>window.LiveReloadOptions={port:${server.port},host:location.hostname,https:${server.scheme === 'https'}};</script>`;
|
|
103
|
+
}
|
|
104
|
+
newbody += '<script src="/__internal__/livereload.js"></script>';
|
|
105
|
+
newbody += body.substring(index);
|
|
106
|
+
return newbody;
|
|
107
|
+
}
|
|
108
|
+
return body;
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Injects meta tags
|
|
113
|
+
* @param {string} body the html body
|
|
114
|
+
* @param {object} props meta properties
|
|
115
|
+
* @returns {string} the modified body
|
|
116
|
+
*/
|
|
117
|
+
injectMeta(body, props) {
|
|
118
|
+
const match = body.match(/<\/head>/i);
|
|
119
|
+
if (!match) {
|
|
120
|
+
return body;
|
|
121
|
+
}
|
|
122
|
+
const text = Object.entries(props).map(([property, content]) => {
|
|
123
|
+
const c = content
|
|
124
|
+
.replace(/&/g, '&')
|
|
125
|
+
.replace(/"/g, '"');
|
|
126
|
+
return `<meta property="${property}" content="${c}">`;
|
|
127
|
+
}).join('\n');
|
|
128
|
+
|
|
129
|
+
const { index } = match;
|
|
130
|
+
return `${body.substring(0, index)}${text}${body.substring(index)}`;
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Computes a path to store a cache objet from a url
|
|
135
|
+
* @param {string} url the url
|
|
136
|
+
* @param {string} directory a local directory path
|
|
137
|
+
* @returns {string} the computed path
|
|
138
|
+
*/
|
|
139
|
+
computePathForCache(url, directory) {
|
|
140
|
+
const u = new URL(url);
|
|
141
|
+
const { pathname, search } = u;
|
|
142
|
+
let fileName = pathname.substring(1) || 'index.html';
|
|
143
|
+
if (search) {
|
|
144
|
+
let qs = search.substring(1); // remove leading '?'
|
|
145
|
+
if (fileName.length + qs.length > 255) {
|
|
146
|
+
// try with query string as md5
|
|
147
|
+
qs = crypto.createHash('md5').update(qs).digest('hex');
|
|
148
|
+
}
|
|
149
|
+
if (fileName.length + qs.length <= 255) {
|
|
150
|
+
const index = fileName.lastIndexOf('.');
|
|
151
|
+
if (index > -1) {
|
|
152
|
+
// inject qs before extension
|
|
153
|
+
fileName = `${fileName.substring(0, index)}!${qs}${fileName.substring(index)}`;
|
|
154
|
+
} else {
|
|
155
|
+
fileName = `${fileName}!${qs}`;
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
// still too long, use md5 as filename
|
|
159
|
+
fileName = crypto.createHash('md5').update(`${fileName}${search.substring(1)}`).digest('hex');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return path.resolve(directory, fileName);
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Writes a partial request object to a local file
|
|
167
|
+
* @param {string} url the request url
|
|
168
|
+
* @param {string} directory a local directory path
|
|
169
|
+
* @param {Object} ret the partial request object ({ body, headers, status })
|
|
170
|
+
* @param {Logger} logger a logger
|
|
171
|
+
*/
|
|
172
|
+
async writeToCache(url, directory, { body, headers, status }, logger) {
|
|
173
|
+
try {
|
|
174
|
+
const filePath = utils.computePathForCache(url, directory);
|
|
175
|
+
const parent = path.dirname(filePath);
|
|
176
|
+
logger.debug(`Not in cache, saving: ${filePath}`);
|
|
177
|
+
await fs.ensureDir(parent);
|
|
178
|
+
await fs.writeFile(filePath, body);
|
|
179
|
+
await fs.writeJSON(`${filePath}.json`, { headers, status });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
logger.error(error);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Returns a partial request object ({ body, headers, status }) from a local file
|
|
187
|
+
* @param {string} url the request url
|
|
188
|
+
* @param {string} directory a local directory path
|
|
189
|
+
* @param {Logger} logger a logger
|
|
190
|
+
* @returns {Object} the partial request object ({ body, headers, status }). Null if not found.
|
|
191
|
+
*/
|
|
192
|
+
async getFromCache(url, directory, logger) {
|
|
193
|
+
try {
|
|
194
|
+
const filePath = utils.computePathForCache(url, directory);
|
|
195
|
+
logger.debug(`Trying from cache first: ${filePath}`);
|
|
196
|
+
|
|
197
|
+
if (await fs.pathExists(filePath)) {
|
|
198
|
+
const body = await fs.readFile(filePath);
|
|
199
|
+
const { headers, status } = await fs.readJSON(`${filePath}.json`);
|
|
200
|
+
return { body, headers, status };
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
logger.error(error);
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Fetches the content from the url and streams it back to the response.
|
|
210
|
+
* @param {RequestContext} ctx Context
|
|
211
|
+
* @param {string} url The url to fetch from
|
|
212
|
+
* @param {Request} req The original express request
|
|
213
|
+
* @param {Response} res The express response
|
|
214
|
+
* @param {object} opts additional request options
|
|
215
|
+
* @return {Promise} A promise that resolves when the stream is done.
|
|
216
|
+
*/
|
|
217
|
+
async proxyRequest(ctx, url, req, res, opts = {}) {
|
|
218
|
+
ctx.log.debug(`Proxy ${req.method} request to ${url}`);
|
|
219
|
+
|
|
220
|
+
if (opts.cacheDirectory) {
|
|
221
|
+
const cached = await utils.getFromCache(url, opts.cacheDirectory, ctx.log);
|
|
222
|
+
if (cached) {
|
|
223
|
+
res
|
|
224
|
+
.status(cached.status)
|
|
225
|
+
.set(cached.headers)
|
|
226
|
+
.send(cached.body);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let body;
|
|
232
|
+
// GET and HEAD requests can't have a body
|
|
233
|
+
if (!['GET', 'HEAD'].includes(req.method)) {
|
|
234
|
+
body = new PassThrough();
|
|
235
|
+
req.pipe(body);
|
|
236
|
+
}
|
|
237
|
+
const stream = new PassThrough();
|
|
238
|
+
req.pipe(stream);
|
|
239
|
+
const headers = {
|
|
240
|
+
'x-forwarded-host': `localhost:${ctx.config.server.port}`,
|
|
241
|
+
'x-forwarded-scheme': 'http',
|
|
242
|
+
...req.headers,
|
|
243
|
+
...(opts.headers || {}),
|
|
244
|
+
};
|
|
245
|
+
// preserve hlx-auth-token cookie
|
|
246
|
+
const cookies = cookie.parse(headers.cookie || '');
|
|
247
|
+
delete headers.cookie;
|
|
248
|
+
const hlxAuthToken = cookies['hlx-auth-token'];
|
|
249
|
+
if (hlxAuthToken) {
|
|
250
|
+
headers.cookie = new URLSearchParams({
|
|
251
|
+
'hlx-auth-token': hlxAuthToken,
|
|
252
|
+
}).toString();
|
|
253
|
+
}
|
|
254
|
+
delete headers.connection;
|
|
255
|
+
delete headers['proxy-connection'];
|
|
256
|
+
delete headers.host;
|
|
257
|
+
const ret = await getFetch()(url, {
|
|
258
|
+
method: req.method,
|
|
259
|
+
headers,
|
|
260
|
+
cache: 'no-store',
|
|
261
|
+
body,
|
|
262
|
+
redirect: 'manual',
|
|
263
|
+
});
|
|
264
|
+
const contentType = ret.headers.get('content-type') || 'text/plain';
|
|
265
|
+
const level = utils.status2level(ret.status, true);
|
|
266
|
+
ctx.log[level](`Proxy ${req.method} request to ${url}: ${ret.status} (${contentType})`);
|
|
267
|
+
|
|
268
|
+
// because fetch decodes the response, we need to reset content encoding and length
|
|
269
|
+
const respHeaders = Object.fromEntries(ret.headers.entries());
|
|
270
|
+
delete respHeaders['content-encoding'];
|
|
271
|
+
delete respHeaders['content-length'];
|
|
272
|
+
delete respHeaders['x-frame-options'];
|
|
273
|
+
delete respHeaders['content-security-policy'];
|
|
274
|
+
respHeaders['access-control-allow-origin'] = '*';
|
|
275
|
+
respHeaders.via = `${ret.httpVersion ?? '1.0'} ${new URL(url).hostname}`;
|
|
276
|
+
|
|
277
|
+
if (ret.status === 404 && contentType.indexOf('text/html') === 0 && opts.file404html) {
|
|
278
|
+
ctx.log.debug('serve local 404.html', opts.file404html);
|
|
279
|
+
let textBody = await fs.readFile(opts.file404html, 'utf-8');
|
|
280
|
+
if (opts.injectLiveReload) {
|
|
281
|
+
textBody = utils.injectLiveReloadScript(textBody, ctx.config.server);
|
|
282
|
+
}
|
|
283
|
+
res
|
|
284
|
+
.status(404)
|
|
285
|
+
.set(respHeaders)
|
|
286
|
+
.send(textBody);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const isHTML = ret.status === 200 && contentType.indexOf('text/html') === 0;
|
|
291
|
+
const livereload = isHTML && opts.injectLiveReload;
|
|
292
|
+
const replaceHead = isHTML && opts.headHtml && opts.headHtml.isModified;
|
|
293
|
+
const doIndex = isHTML && opts.indexer && url.indexOf('.plain.html') < 0;
|
|
294
|
+
|
|
295
|
+
if (isHTML) {
|
|
296
|
+
let respBody;
|
|
297
|
+
let textBody;
|
|
298
|
+
if (contentType.startsWith('text/')) {
|
|
299
|
+
textBody = await ret.text();
|
|
300
|
+
} else {
|
|
301
|
+
respBody = await ret.buffer();
|
|
302
|
+
}
|
|
303
|
+
const lines = ['----------------------------->'];
|
|
304
|
+
if (ctx.log.level === 'silly') {
|
|
305
|
+
lines.push(`${req.method} ${url}`);
|
|
306
|
+
Object.entries(headers).forEach(([name, value]) => {
|
|
307
|
+
lines.push(`${name}: ${value}`);
|
|
308
|
+
});
|
|
309
|
+
lines.push('');
|
|
310
|
+
lines.push('<-----------------------------');
|
|
311
|
+
lines.push('');
|
|
312
|
+
lines.push(`http/${ret.httpVersion} ${ret.status} ${ret.statusText}`);
|
|
313
|
+
ret.headers.forEach((name, value) => {
|
|
314
|
+
lines.push(`${name}: ${value}`);
|
|
315
|
+
});
|
|
316
|
+
lines.push('');
|
|
317
|
+
if (respBody) {
|
|
318
|
+
lines.push(`<binary ${respBody.length} bytes>`);
|
|
319
|
+
} else {
|
|
320
|
+
lines.push(textBody);
|
|
321
|
+
}
|
|
322
|
+
ctx.log.trace(lines.join('\n'));
|
|
323
|
+
}
|
|
324
|
+
if (replaceHead) {
|
|
325
|
+
textBody = await opts.headHtml.replace(textBody);
|
|
326
|
+
}
|
|
327
|
+
if (livereload) {
|
|
328
|
+
textBody = utils.injectLiveReloadScript(textBody, ctx.config.server);
|
|
329
|
+
}
|
|
330
|
+
textBody = utils.injectMeta(textBody, {
|
|
331
|
+
'hlx:proxyUrl': url,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (doIndex) {
|
|
335
|
+
opts.indexer.onData(url, {
|
|
336
|
+
body: textBody,
|
|
337
|
+
headers: respHeaders,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (opts.cacheDirectory) {
|
|
342
|
+
await utils.writeToCache(
|
|
343
|
+
url,
|
|
344
|
+
opts.cacheDirectory,
|
|
345
|
+
{
|
|
346
|
+
body: respBody || textBody,
|
|
347
|
+
headers: respHeaders,
|
|
348
|
+
status: ret.status,
|
|
349
|
+
},
|
|
350
|
+
ctx.log,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
res
|
|
355
|
+
.status(ret.status)
|
|
356
|
+
.set(respHeaders)
|
|
357
|
+
.send(respBody || textBody);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (opts.cacheDirectory) {
|
|
362
|
+
const buffer = await ret.buffer();
|
|
363
|
+
await utils.writeToCache(
|
|
364
|
+
url,
|
|
365
|
+
opts.cacheDirectory,
|
|
366
|
+
{
|
|
367
|
+
body: buffer,
|
|
368
|
+
headers: respHeaders,
|
|
369
|
+
status: ret.status,
|
|
370
|
+
},
|
|
371
|
+
ctx.log,
|
|
372
|
+
);
|
|
373
|
+
res
|
|
374
|
+
.status(ret.status)
|
|
375
|
+
.set(respHeaders)
|
|
376
|
+
.send(buffer);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
res
|
|
381
|
+
.status(ret.status)
|
|
382
|
+
.set(respHeaders);
|
|
383
|
+
ret.body.pipe(res);
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Generates a random string of the given `length` consisting of alpha numerical characters.
|
|
388
|
+
* if `hex` is {@code true}, the string will only consist of hexadecimal digits.
|
|
389
|
+
* @param {number}length length of the string.
|
|
390
|
+
* @param {boolean} hex returns a hex string if {@code true}
|
|
391
|
+
* @returns {String} a random string.
|
|
392
|
+
*/
|
|
393
|
+
randomChars(length, hex = false) {
|
|
394
|
+
if (length === 0) {
|
|
395
|
+
return '';
|
|
396
|
+
}
|
|
397
|
+
if (hex) {
|
|
398
|
+
return crypto.randomBytes(Math.round(length / 2)).toString('hex').substring(0, length);
|
|
399
|
+
}
|
|
400
|
+
const str = crypto.randomBytes(length).toString('base64');
|
|
401
|
+
return str.substring(0, length);
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Checks if the given port is already in use on any addr. This is used to prevent starting a
|
|
406
|
+
* server on the same port with an existing socket bound to 0.0.0.0 and SO_REUSEADDR.
|
|
407
|
+
* @param port
|
|
408
|
+
@param addr * @return {Promise} that resolves `true` if the port is in use.
|
|
409
|
+
*/
|
|
410
|
+
checkPortInUse(port, addr = '0.0.0.0') {
|
|
411
|
+
return new Promise((resolve, reject) => {
|
|
412
|
+
let socket;
|
|
413
|
+
|
|
414
|
+
const cleanUp = () => {
|
|
415
|
+
if (socket) {
|
|
416
|
+
socket.removeAllListeners('connect');
|
|
417
|
+
socket.removeAllListeners('error');
|
|
418
|
+
socket.end();
|
|
419
|
+
socket.destroy();
|
|
420
|
+
socket.unref();
|
|
421
|
+
socket = null;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
socket = new Socket();
|
|
426
|
+
socket.once('error', (err) => {
|
|
427
|
+
if (err.code !== 'ECONNREFUSED') {
|
|
428
|
+
reject(err);
|
|
429
|
+
} else {
|
|
430
|
+
resolve(false);
|
|
431
|
+
}
|
|
432
|
+
cleanUp();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
socket.connect(port, addr, () => {
|
|
436
|
+
resolve(true);
|
|
437
|
+
cleanUp();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Rewrites all absolute urls to the proxy host with relative ones.
|
|
444
|
+
* @param {Buffer} html
|
|
445
|
+
* @param {string} host
|
|
446
|
+
* @returns {Buffer}
|
|
447
|
+
*/
|
|
448
|
+
rewriteUrl(html, host) {
|
|
449
|
+
const hostPattern = host.replaceAll('.', '\\.');
|
|
450
|
+
let text = html.toString('utf-8');
|
|
451
|
+
const re = new RegExp(`(src|href)\\s*=\\s*(["'])${hostPattern}(/.*?)?(['"])`, 'gm');
|
|
452
|
+
text = text.replaceAll(re, (match, arg, q1, value, q2) => (`${arg}=${q1}${value || '/'}${q2}`));
|
|
453
|
+
return Buffer.from(text, 'utf-8');
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
export default Object.freeze(utils);
|
package/src/up.cmd.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2018 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import fse from 'fs-extra';
|
|
14
|
+
import chalk from 'chalk-template';
|
|
15
|
+
import chokidar from 'chokidar';
|
|
16
|
+
import { HelixProject } from './server/HelixProject.js';
|
|
17
|
+
import GitUtils from './git-utils.js';
|
|
18
|
+
import pkgJson from './package.cjs';
|
|
19
|
+
import { getFetch } from './fetch-utils.js';
|
|
20
|
+
import { AbstractServerCommand } from './abstract-server.cmd.js';
|
|
21
|
+
|
|
22
|
+
export default class UpCommand extends AbstractServerCommand {
|
|
23
|
+
withLiveReload(value) {
|
|
24
|
+
this._liveReload = value;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
withUrl(value) {
|
|
29
|
+
this._url = value;
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
withPrintIndex(value) {
|
|
34
|
+
this._printIndex = value;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async doStop() {
|
|
39
|
+
await super.doStop();
|
|
40
|
+
if (this._watcher) {
|
|
41
|
+
const watcher = this._watcher;
|
|
42
|
+
delete this._watcher;
|
|
43
|
+
await watcher.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async init() {
|
|
48
|
+
await super.init();
|
|
49
|
+
// check for git repository
|
|
50
|
+
try {
|
|
51
|
+
const stat = await fse.lstat(path.resolve(this.directory, '.git'));
|
|
52
|
+
if (stat.isFile()) {
|
|
53
|
+
throw Error('git submodules are not supported.');
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
if (e.code === 'ENOENT') {
|
|
57
|
+
throw Error('hlx up needs local git repository.');
|
|
58
|
+
}
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// init dev default file params
|
|
63
|
+
this._project = new HelixProject()
|
|
64
|
+
.withCwd(this.directory)
|
|
65
|
+
.withLiveReload(this._liveReload)
|
|
66
|
+
.withLogger(this._logger)
|
|
67
|
+
.withKill(this._kill)
|
|
68
|
+
.withPrintIndex(this._printIndex);
|
|
69
|
+
this.log.info(chalk`{yellow ___ ________ ___ __ __ v${pkgJson.version}}`);
|
|
70
|
+
this.log.info(chalk`{yellow / | / ____/ |/ / _____(_)___ ___ __ __/ /___ _/ /_____ _____}`);
|
|
71
|
+
this.log.info(chalk`{yellow / /| | / __/ / /|_/ / / ___/ / __ \`__ \\/ / / / / __ \`/ __/ __ \\/ ___/}`);
|
|
72
|
+
this.log.info(chalk`{yellow / ___ |/ /___/ / / / (__ ) / / / / / / /_/ / / /_/ / /_/ /_/ / /}`);
|
|
73
|
+
this.log.info(chalk`{yellow /_/ |_/_____/_/ /_/ /____/_/_/ /_/ /_/\\__,_/_/\\__,_/\\__/\\____/_/}`);
|
|
74
|
+
this.log.info('');
|
|
75
|
+
|
|
76
|
+
const ref = await GitUtils.getBranch(this.directory);
|
|
77
|
+
const gitUrl = await GitUtils.getOriginURL(this.directory, { ref });
|
|
78
|
+
let explicitURL = true;
|
|
79
|
+
if (!this._url) {
|
|
80
|
+
explicitURL = false;
|
|
81
|
+
// check if remote already has the `ref`
|
|
82
|
+
await this.verifyUrl(gitUrl, ref);
|
|
83
|
+
}
|
|
84
|
+
this._project.withProxyUrl(this._url);
|
|
85
|
+
await this.initSeverOptions();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await this._project.init();
|
|
89
|
+
|
|
90
|
+
if (!explicitURL) {
|
|
91
|
+
this.watchGit();
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
throw Error(`Unable to start AEM: ${e.message}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this._project.on('stopped', async () => {
|
|
98
|
+
await this.stop();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async verifyUrl(gitUrl, inref) {
|
|
103
|
+
let ref = inref;
|
|
104
|
+
|
|
105
|
+
// replace `/` by `-` in ref.
|
|
106
|
+
ref = ref.replace(/\//g, '-');
|
|
107
|
+
this._url = `https://${ref}--${gitUrl.repo}--${gitUrl.owner}.hlx.page`;
|
|
108
|
+
// check length limit
|
|
109
|
+
if (this._url.split('.')
|
|
110
|
+
.map((part) => part.replace(/^https:\/\//, ''))
|
|
111
|
+
.some((part) => part.length > 63)) {
|
|
112
|
+
this.log.error(chalk`URL {yellow ${this._url}} exceeds the 63 character limit for DNS labels.`);
|
|
113
|
+
this.log.error(chalk`Please use a shorter branch name or a shorter repository name.`);
|
|
114
|
+
await this.stop();
|
|
115
|
+
throw Error('branch name too long');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const fstabUrl = `${this._url}/fstab.yaml`;
|
|
119
|
+
const resp = await getFetch()(fstabUrl);
|
|
120
|
+
await resp.buffer();
|
|
121
|
+
if (!resp.ok) {
|
|
122
|
+
if (ref === 'main') {
|
|
123
|
+
this.log.warn(chalk`Unable to verify {yellow main} branch via {blue ${fstabUrl}} (${resp.status}). Maybe not pushed yet?`);
|
|
124
|
+
} else {
|
|
125
|
+
this.log.warn(chalk`Unable to verify {yellow ${ref}} branch on {blue ${fstabUrl}} (${resp.status}). Fallback to {yellow main} branch.`);
|
|
126
|
+
this._url = `https://main--${gitUrl.repo}--${gitUrl.owner}.hlx.page`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Watches the git repository for changes and restarts the server if necessary.
|
|
133
|
+
*/
|
|
134
|
+
watchGit() {
|
|
135
|
+
let timer = null;
|
|
136
|
+
|
|
137
|
+
this._watcher = chokidar.watch(path.resolve(this._project.directory, '.git'), {
|
|
138
|
+
persistent: true,
|
|
139
|
+
ignoreInitial: true,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
this._watcher.on('all', (eventType, file) => {
|
|
143
|
+
if (file.endsWith('.git/HEAD') || file.endsWith('.git\\HEAD') || file.match(/\.git[/\\]refs[/\\]heads[/\\].+/)) {
|
|
144
|
+
if (timer) {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
}
|
|
147
|
+
// debounce a bit in case several files are changed at once
|
|
148
|
+
timer = setTimeout(async () => {
|
|
149
|
+
timer = null;
|
|
150
|
+
if (!this._watcher) {
|
|
151
|
+
// watcher was closed in the meantime
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
// restart if any of the files is not ignored
|
|
156
|
+
this.log.info('git HEAD or remotes changed, reconfiguring server...');
|
|
157
|
+
const ref = await GitUtils.getBranch(this.directory);
|
|
158
|
+
const gitUrl = await GitUtils.getOriginURL(this.directory, { ref });
|
|
159
|
+
await this.verifyUrl(gitUrl, ref);
|
|
160
|
+
this._project.withProxyUrl(this._url);
|
|
161
|
+
await this._project.initHeadHtml();
|
|
162
|
+
this.log.info(`Updated proxy to ${this._url}`);
|
|
163
|
+
this.emit('changed', this);
|
|
164
|
+
} catch {
|
|
165
|
+
// ignore
|
|
166
|
+
}
|
|
167
|
+
}, 100);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|