@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.
@@ -0,0 +1,276 @@
1
+ /*
2
+ * Copyright 2020 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
+ // eslint-disable-next-line max-classes-per-file
13
+ import fs from 'fs';
14
+ import chokidar from 'chokidar';
15
+ import WebSocket from 'faye-websocket';
16
+ import { EventEmitter } from 'events';
17
+ import { createRequire } from 'module';
18
+
19
+ const require = createRequire(import.meta.url);
20
+
21
+ /**
22
+ * Client connection for the live reload server.
23
+ */
24
+ class ClientConnection extends EventEmitter {
25
+ static nextId() {
26
+ ClientConnection.counter = (ClientConnection.counter || 0) + 1;
27
+ return `ws${ClientConnection.counter}`;
28
+ }
29
+
30
+ constructor(req, socket, head) {
31
+ super();
32
+ this.id = ClientConnection.nextId();
33
+ this.ws = new WebSocket(req, socket, head);
34
+ this.ws.onmessage = this._onMessage.bind(this);
35
+ this.ws.onclose = this._onClose.bind(this);
36
+ }
37
+
38
+ _onMessage(event) {
39
+ let data = {};
40
+ try {
41
+ data = JSON.parse(event.data);
42
+ } catch {
43
+ // ignore
44
+ }
45
+ switch (data.command) {
46
+ case 'hello':
47
+ return this._cmdHello(data);
48
+ case 'info':
49
+ return this._cmdInfo(data);
50
+ default:
51
+ return {};
52
+ }
53
+ }
54
+
55
+ _onClose(event) {
56
+ if (this.ws) {
57
+ this.ws.close();
58
+ this.ws = null;
59
+ }
60
+ this.emit('end', event);
61
+ }
62
+
63
+ _cmdHello() {
64
+ this._send({
65
+ command: 'hello',
66
+ protocols: [
67
+ 'http://livereload.com/protocols/official-7',
68
+ ],
69
+ serverName: 'aem-simulator',
70
+ });
71
+ }
72
+
73
+ _cmdInfo(data) {
74
+ if (data) {
75
+ this.plugins = data.plugins;
76
+ this.url = data.url;
77
+ }
78
+ return { ...data || {}, id: this.id, url: this.url };
79
+ }
80
+
81
+ _send(data) {
82
+ if (this.ws) {
83
+ this.ws.send(JSON.stringify(data));
84
+ }
85
+ }
86
+
87
+ sendReload(files) {
88
+ files.forEach((file) => {
89
+ this._send({
90
+ command: 'reload',
91
+ path: file,
92
+ liveCSS: true,
93
+ reloadMissingCSS: true,
94
+ liveImg: true,
95
+ });
96
+ }, this);
97
+ }
98
+
99
+ sendAlert(message) {
100
+ this._send({
101
+ command: 'alert',
102
+ message,
103
+ });
104
+ }
105
+
106
+ close() {
107
+ this._onClose({});
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Live reload file watcher and server.
113
+ */
114
+ export default class LiveReload extends EventEmitter {
115
+ constructor(logger) {
116
+ super();
117
+ // file to request mapping
118
+ this._fileMapping = new Map();
119
+ // pending requests by request id
120
+ this._pending = new Map();
121
+ this._logger = logger;
122
+
123
+ // client connections
124
+ this._connections = {};
125
+ this._liveReloadJSPath = require.resolve('livereload-js/dist/livereload.js');
126
+ }
127
+
128
+ get log() {
129
+ return this._logger;
130
+ }
131
+
132
+ startRequest(requestId, pathname) {
133
+ this._pending.set(requestId, {
134
+ pathname,
135
+ files: [],
136
+ });
137
+ }
138
+
139
+ endRequest(requestId) {
140
+ const info = this._pending.get(requestId);
141
+ if (!info) {
142
+ this.log.debug('unable to register accessed files. info does not exist: ', requestId);
143
+ return;
144
+ }
145
+ this._pending.delete(requestId);
146
+ this.registerFiles(info.files, info.pathname);
147
+ }
148
+
149
+ registerFiles(files, pathName) {
150
+ this._watcher.add(files);
151
+ files.forEach((file) => {
152
+ this._fileMapping.set(file, pathName);
153
+ });
154
+ }
155
+
156
+ registerFile(requestId, filePath) {
157
+ const info = this._pending.get(requestId);
158
+ if (info) {
159
+ info.files.push(filePath);
160
+ } else {
161
+ this.log.debug(`unable to register file ${filePath}. info for ${requestId} does not exit.`);
162
+ }
163
+ }
164
+
165
+ async init(app, httpServer) {
166
+ this._server = httpServer;
167
+ app.get('/__internal__/livereload.js', this._serveLiveReload.bind(this));
168
+ httpServer.on('upgrade', this._onSvrUpgrade.bind(this));
169
+ httpServer.on('error', this._onSvrError.bind(this));
170
+ httpServer.on('close', this._onSvrClose.bind(this));
171
+ this._initWatcher();
172
+ }
173
+
174
+ _initWatcher() {
175
+ let timer = null;
176
+ let modifiedFiles = {};
177
+
178
+ this._watcher = chokidar.watch([], {
179
+ ignored: [/(.*\.swx|.*\.swp|.*~)/],
180
+ persistent: true,
181
+ ignoreInitial: true,
182
+ });
183
+
184
+ this._watcher.on('all', (eventType, file) => {
185
+ modifiedFiles[file] = true;
186
+ if (timer) {
187
+ clearTimeout(timer);
188
+ }
189
+ // debounce a bit in case several files are changed at once
190
+ timer = setTimeout(async () => {
191
+ timer = null;
192
+ // only proceed if watcher not closed.
193
+ if (this._watcher) {
194
+ const files = Object.keys(modifiedFiles);
195
+ modifiedFiles = {};
196
+
197
+ // inform clients
198
+ await this.changed(files);
199
+ }
200
+ }, 100);
201
+ });
202
+ }
203
+
204
+ async stop() {
205
+ if (this._watcher) {
206
+ await this._watcher.close();
207
+ delete this._watcher;
208
+ }
209
+ this._onSvrClose();
210
+ this.log.debug('livereload stopped.');
211
+ }
212
+
213
+ _onSvrUpgrade(req, socket, head) {
214
+ const cx = new ClientConnection(req, socket, head);
215
+ this._connections[cx.id] = cx;
216
+
217
+ socket.on('error', (e) => {
218
+ if (e.code === 'ECONNRESET' || e.code === 'EBADF') {
219
+ return;
220
+ }
221
+ this._onSvrError(e);
222
+ });
223
+
224
+ cx.once('end', () => {
225
+ this.log.debug(`websocket connection closed ${cx.id} (url: ${cx.url})`);
226
+ delete this._connections[cx.id];
227
+ });
228
+ }
229
+
230
+ _onSvrClose() {
231
+ Object.values(this._connections).forEach((cx) => {
232
+ try {
233
+ cx.close();
234
+ } catch (e) {
235
+ this.log.error('error closing connection', e);
236
+ }
237
+ }, this);
238
+ }
239
+
240
+ _onSvrError(e) {
241
+ this.log.error(e);
242
+ }
243
+
244
+ _serveLiveReload(req, res) {
245
+ res.setHeader('content-type', 'application/javascript');
246
+ fs.createReadStream(this._liveReloadJSPath).pipe(res);
247
+ }
248
+
249
+ async changed(files) {
250
+ this.log.debug(`changed files: ${files}`);
251
+
252
+ // map each file to the registered source
253
+ const sources = new Set();
254
+ files.forEach((file) => {
255
+ const mapping = this._fileMapping.get(file);
256
+ if (mapping) {
257
+ sources.add(mapping);
258
+ }
259
+ });
260
+ const modified = Array.from(sources);
261
+ await this.emit('modified', modified);
262
+
263
+ Object.values(this._connections).forEach((cx) => {
264
+ this.log.debug(`reloading client ${cx.id} (url: ${cx.url})`);
265
+ cx.sendReload(modified);
266
+ });
267
+ }
268
+
269
+ alert(message) {
270
+ this.log.debug(`alert: ${message}`);
271
+ Object.values(this._connections).forEach((cx) => {
272
+ this.log.debug(`alert client ${cx.id} (url: ${cx.url})`);
273
+ cx.sendAlert(message);
274
+ });
275
+ }
276
+ }
@@ -0,0 +1,205 @@
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 { parse } from 'url';
13
+ import utils from './utils.js';
14
+
15
+ /**
16
+ * Context that is used during request handling.
17
+ *
18
+ * @type {module.RequestContext}
19
+ */
20
+ export default class RequestContext {
21
+ constructor(request, cfg) {
22
+ // see https://github.com/nodejs/node/issues/36550
23
+ const req = {
24
+ ...request,
25
+ };
26
+ const { url } = req;
27
+ this._cfg = cfg || {};
28
+ this._url = url;
29
+ const purl = parse(url);
30
+ this._path = purl.pathname || '/';
31
+ this._queryString = purl.search || '';
32
+ this._selector = '';
33
+ this._extension = '';
34
+ this._headers = req.headers || {};
35
+ this._method = req.method || 'GET';
36
+ this._params = req.query || {};
37
+ this._requestId = utils.randomChars(32);
38
+ this._logger = cfg.log;
39
+
40
+ if (req.body && Object.entries(req.body).length > 0) {
41
+ this._body = req.body;
42
+ }
43
+ const lastSlash = this._path.lastIndexOf('/');
44
+ if (lastSlash === this._path.length - 1) {
45
+ // directory request
46
+ const index = 'index.html';
47
+ // append index and remove multiple slashes
48
+ this._path = `${this._path}${index}`.replace(/\/+/g, '/');
49
+ }
50
+ const lastDot = this._path.lastIndexOf('.');
51
+ let relPath = lastDot >= 0 ? this._path.substring(0, lastDot) : this._path;
52
+
53
+ if (lastDot > lastSlash) {
54
+ this._extension = this._path.substring(lastDot + 1);
55
+ } else {
56
+ // append .html
57
+ this._extension = 'html';
58
+ this._path += '.html';
59
+ }
60
+ // check for selector
61
+ const selDot = relPath.lastIndexOf('.');
62
+ if (selDot > lastSlash) {
63
+ this._selector = relPath.substring(selDot + 1);
64
+ relPath = relPath.substring(0, selDot);
65
+ }
66
+ this._relPath = this._path;
67
+
68
+ // prepend any content repository path
69
+ const repoPath = '';
70
+ if (repoPath && repoPath !== '/') {
71
+ relPath = repoPath + relPath;
72
+ }
73
+
74
+ this._resourcePath = relPath;
75
+ }
76
+
77
+ /**
78
+ * the original request url
79
+ */
80
+ get url() {
81
+ return this._url;
82
+ }
83
+
84
+ /**
85
+ * the request path, including any directoryIndex mapping.
86
+ * @returns {*|string}
87
+ */
88
+ get path() {
89
+ return this._path;
90
+ }
91
+
92
+ /**
93
+ * The helix project configuration.
94
+ * @returns {HelixProject}
95
+ */
96
+ get config() {
97
+ return this._cfg;
98
+ }
99
+
100
+ /**
101
+ * The request body.
102
+ * @returns {Object}
103
+ */
104
+ get body() {
105
+ return this._body;
106
+ }
107
+
108
+ /**
109
+ * The path to the resource in the repository.
110
+ * @returns {string}
111
+ */
112
+ get resourcePath() {
113
+ return this._resourcePath;
114
+ }
115
+
116
+ /**
117
+ * The file extension of the request path.
118
+ * @returns {string|*}
119
+ */
120
+ get extension() {
121
+ return this._extension;
122
+ }
123
+
124
+ /**
125
+ * the selector of the request path.
126
+ * @returns {string|string}
127
+ */
128
+ get selector() {
129
+ return this._selector;
130
+ }
131
+
132
+ /**
133
+ * The client request headers.
134
+ * @returns {any | {}}
135
+ */
136
+ get headers() {
137
+ return this._headers;
138
+ }
139
+
140
+ /**
141
+ * The request method
142
+ * @returns {*|string}
143
+ */
144
+ get method() {
145
+ return this._method;
146
+ }
147
+
148
+ /**
149
+ * The request params (query)
150
+ * @returns {any | {}}
151
+ */
152
+ get params() {
153
+ return this._params;
154
+ }
155
+
156
+ /**
157
+ * The relative path. i.e. the request path without the mount path.
158
+ * @returns {*}
159
+ */
160
+ get relPath() {
161
+ return this._relPath;
162
+ }
163
+
164
+ /**
165
+ * Returns the queryString of the url (including the ?)
166
+ * @returns {string}
167
+ */
168
+ get queryString() {
169
+ return this._queryString;
170
+ }
171
+
172
+ /**
173
+ * The request id.
174
+ * @returns {String}
175
+ */
176
+ get requestId() {
177
+ return this._requestId;
178
+ }
179
+
180
+ /**
181
+ * The logger;
182
+ * @returns {Logger}
183
+ */
184
+ get log() {
185
+ return this._logger;
186
+ }
187
+
188
+ get json() {
189
+ const o = {
190
+ url: this.url,
191
+ queryString: this.queryString,
192
+ resourcePath: this.resourcePath,
193
+ path: this.path,
194
+ selector: this.selector,
195
+ extension: this.extension,
196
+ method: this.method,
197
+ headers: this.headers,
198
+ params: this.params,
199
+ };
200
+ if (this.body) {
201
+ o.body = this.body;
202
+ }
203
+ return o;
204
+ }
205
+ }