@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,30 @@
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 { HelixImportServer } from './HelixImportServer.js';
13
+ import { BaseProject } from './BaseProject.js';
14
+
15
+ export class HelixImportProject extends BaseProject {
16
+ constructor() {
17
+ super(HelixImportServer);
18
+ }
19
+
20
+ async start() {
21
+ this.log.debug('Launching AEM import server for importing content...');
22
+ await super.start();
23
+ return this;
24
+ }
25
+
26
+ async doStop() {
27
+ this.log.debug('Stopping AEM import server..');
28
+ await super.doStop();
29
+ }
30
+ }
@@ -0,0 +1,207 @@
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 { promisify } from 'util';
13
+ import path from 'path';
14
+ import { PassThrough } from 'stream';
15
+ import { getFetch, resetContext } from '../fetch-utils.js';
16
+ import utils from './utils.js';
17
+ import RequestContext from './RequestContext.js';
18
+ import { asyncHandler, BaseServer } from './BaseServer.js';
19
+
20
+ export class HelixImportServer extends BaseServer {
21
+ /**
22
+ * Proxy Mode route handler
23
+ * @param {Express.Request} req request
24
+ * @param {Express.Response} res response
25
+ */
26
+ async handleToolsRequest(req, res) {
27
+ const sendFile = promisify(res.sendFile).bind(res);
28
+ const ctx = new RequestContext(req, this._project);
29
+ const { log } = this;
30
+
31
+ // try to serve static
32
+ try {
33
+ const filePath = path.join(this._project.directory, ctx.path);
34
+ if (path.relative(this._project.directory, filePath).startsWith('..')) {
35
+ log.info(`refuse to serve file outside the project directory: ${filePath}`);
36
+ res.status(403).send('');
37
+ return;
38
+ }
39
+ log.debug('trying to serve local file', filePath);
40
+ await sendFile(filePath, {
41
+ dotfiles: 'allow',
42
+ headers: {
43
+ 'access-control-allow-origin': '*',
44
+ },
45
+ });
46
+ return;
47
+ } catch (e) {
48
+ // not sure what to do yet here
49
+ // codecov:ignore:start
50
+ /* c8 ignore start */
51
+ log.debug(`Error while delivering resource ${ctx.path} - ${e.stack || e}`);
52
+ // codecov:ignore:end
53
+ /* c8 ignore end */
54
+ }
55
+
56
+ res.status(404).send(`Unknown path: ${ctx.path}`);
57
+ }
58
+
59
+ // eslint-disable-next-line class-methods-use-this
60
+ _makeProxyURL(reqUrl, base) {
61
+ const url = new URL(reqUrl, base);
62
+ url.searchParams.delete('host');
63
+ return url.toString();
64
+ }
65
+
66
+ async _doProxyRequest(ctx, url, host, req, res) {
67
+ ctx.log.debug(`Proxy ${req.method} request to ${url}`);
68
+
69
+ // POST requests have a body
70
+ const isBodyReq = !['GET', 'HEAD'].includes(req.method);
71
+
72
+ // do not cache POST requests
73
+ if (!isBodyReq && this._project.cacheDirectory) {
74
+ const cached = await utils.getFromCache(
75
+ url,
76
+ this._project.cacheDirectory,
77
+ ctx.log,
78
+ );
79
+ if (cached) {
80
+ res
81
+ .status(cached.status)
82
+ .set(cached.headers)
83
+ .cookie('hlx-proxyhost', host)
84
+ .send(cached.body);
85
+ return;
86
+ }
87
+ }
88
+
89
+ let body;
90
+ // pipe body if any
91
+ if (isBodyReq) {
92
+ body = new PassThrough();
93
+ req.pipe(body);
94
+ }
95
+
96
+ const headers = {
97
+ ...req.headers,
98
+ };
99
+ delete headers.cookie;
100
+ delete headers.connection;
101
+ delete headers.host;
102
+ delete headers.referer;
103
+
104
+ const ret = await getFetch(true)(url, {
105
+ method: req.method,
106
+ headers,
107
+ cache: 'no-store',
108
+ redirect: 'manual',
109
+ body,
110
+ });
111
+
112
+ const contentType = ret.headers.get('content-type') || 'text/plain';
113
+ const level = utils.status2level(ret.status, true);
114
+ ctx.log[level](`Proxy ${req.method} request to ${url}: ${ret.status} (${contentType})`);
115
+
116
+ // because fetch decodes the response, we need to reset content encoding and length
117
+ const respHeaders = Object.fromEntries(ret.headers.entries());
118
+ delete respHeaders['content-encoding'];
119
+ delete respHeaders['content-length'];
120
+
121
+ // remove security "constraints"
122
+ delete respHeaders['x-frame-options'];
123
+ delete respHeaders['content-security-policy'];
124
+ respHeaders['access-control-allow-origin'] = '*';
125
+ delete respHeaders['set-cookie'];
126
+
127
+ if (respHeaders.location && !respHeaders.location.startsWith('/')) {
128
+ const u = new URL(respHeaders.location);
129
+ if (u.origin === host) {
130
+ respHeaders.location = u.pathname;
131
+ }
132
+ }
133
+
134
+ let buffer = await ret.buffer();
135
+ if (contentType.includes('html') || contentType.includes('text')) {
136
+ buffer = utils.rewriteUrl(buffer, host);
137
+ }
138
+
139
+ if (!isBodyReq && this._project.cacheDirectory) {
140
+ await utils.writeToCache(
141
+ url,
142
+ this._project.cacheDirectory,
143
+ {
144
+ body: buffer,
145
+ headers: respHeaders,
146
+ status: ret.status,
147
+ },
148
+ ctx.log,
149
+ );
150
+ }
151
+
152
+ res
153
+ .status(ret.status)
154
+ .set(respHeaders)
155
+ .cookie('hlx-proxyhost', host)
156
+ .send(buffer);
157
+ ret.body.pipe(res);
158
+ }
159
+
160
+ /**
161
+ * Proxy Mode route handler
162
+ * @param {Express.Request} req request
163
+ * @param {Express.Response} res response
164
+ */
165
+ async handleProxyModeRequest(req, res) {
166
+ const ctx = new RequestContext(req, this._project);
167
+ const { log } = this;
168
+
169
+ let host = ctx.params?.host;
170
+ if (!host) {
171
+ // first call sets the cookie, next calls use the cookie
172
+ host = req.cookies['hlx-proxyhost'];
173
+ }
174
+
175
+ if (!host) {
176
+ res.status(403).send('Missing host parameter');
177
+ } else {
178
+ try {
179
+ host = new URL(host).origin;
180
+ const url = this._makeProxyURL(ctx.url, host);
181
+ await this._doProxyRequest(ctx, url, host, req, res);
182
+ // codecov:ignore:start
183
+ /* c8 ignore start */
184
+ } catch (err) {
185
+ log.error(`Failed to proxy AEM request ${ctx.path}: ${err.message}`);
186
+ res.status(502).send(`Failed to proxy AEM request: ${err.message}`);
187
+ }
188
+ // codecov:ignore:end
189
+ /* c8 ignore end */
190
+ }
191
+
192
+ this.emit('request', req, res, ctx);
193
+ }
194
+
195
+ async setupApp() {
196
+ await super.setupApp();
197
+ this.app.get('/tools/*', asyncHandler(this.handleToolsRequest.bind(this)));
198
+ const handler = asyncHandler(this.handleProxyModeRequest.bind(this));
199
+ this.app.get('*', handler);
200
+ this.app.post('*', handler);
201
+ }
202
+
203
+ async doStop() {
204
+ await super.doStop();
205
+ await resetContext();
206
+ }
207
+ }
@@ -0,0 +1,130 @@
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 { resolve } from 'path';
13
+ import { lstat } from 'fs/promises';
14
+ import { HelixServer } from './HelixServer.js';
15
+ import { BaseProject } from './BaseProject.js';
16
+ import HeadHtmlSupport from './HeadHtmlSupport.js';
17
+ import Indexer from './Indexer.js';
18
+
19
+ export class HelixProject extends BaseProject {
20
+ constructor() {
21
+ super(HelixServer);
22
+ this._proxyUrl = null;
23
+ this._headHtml = null;
24
+ this._indexer = null;
25
+ this._printIndex = false;
26
+ this._file404html = null;
27
+ }
28
+
29
+ withLiveReload(value) {
30
+ this._server.withLiveReload(value);
31
+ return this;
32
+ }
33
+
34
+ withProxyUrl(value) {
35
+ this._proxyUrl = value;
36
+ return this;
37
+ }
38
+
39
+ withPrintIndex(value) {
40
+ this._printIndex = value;
41
+ return this;
42
+ }
43
+
44
+ get proxyUrl() {
45
+ return this._proxyUrl;
46
+ }
47
+
48
+ get indexer() {
49
+ return this._indexer;
50
+ }
51
+
52
+ get liveReload() {
53
+ // eslint-disable-next-line no-underscore-dangle
54
+ return this._server._liveReload;
55
+ }
56
+
57
+ get file404html() {
58
+ return this._file404html;
59
+ }
60
+
61
+ get headHtml() {
62
+ return this._headHtml;
63
+ }
64
+
65
+ async init() {
66
+ await super.init();
67
+ this._indexer = new Indexer()
68
+ .withLogger(this._logger)
69
+ .withCwd(this._cwd)
70
+ .withPrintIndex(this._printIndex);
71
+ return this;
72
+ }
73
+
74
+ async initHeadHtml() {
75
+ if (this.proxyUrl) {
76
+ this._headHtml = new HeadHtmlSupport({
77
+ directory: this.directory,
78
+ log: this.log,
79
+ proxyUrl: this.proxyUrl,
80
+ });
81
+ await this._headHtml.init();
82
+
83
+ // register local head in live-reload
84
+ if (this.liveReload) {
85
+ this.liveReload.registerFiles([this._headHtml.filePath], '/');
86
+ this.liveReload.on('modified', async (modified) => {
87
+ if (modified.indexOf('/') >= 0) {
88
+ await this._headHtml.loadLocal();
89
+ await this._headHtml.init();
90
+ }
91
+ });
92
+ }
93
+ }
94
+ }
95
+
96
+ async init404Html() {
97
+ if (this.proxyUrl) {
98
+ this._file404html = resolve(this.directory, '404.html');
99
+ try {
100
+ await lstat(this._file404html);
101
+ this.log.debug('detected local 404.html');
102
+ if (this.liveReload) {
103
+ this.liveReload.registerFiles([this._file404html], '/');
104
+ }
105
+ } catch (e) {
106
+ this._file404html = null;
107
+ }
108
+ }
109
+ }
110
+
111
+ async start() {
112
+ this.log.debug('Launching AEM dev server...');
113
+ await super.start();
114
+ await this.initHeadHtml();
115
+ await this.init404Html();
116
+ if (this._indexer) {
117
+ await this._indexer.init();
118
+ }
119
+ return this;
120
+ }
121
+
122
+ async doStop() {
123
+ this.log.debug('Stopping AEM dev server...');
124
+ await super.doStop();
125
+ if (this._indexer) {
126
+ await this._indexer.close();
127
+ delete this._indexer;
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,120 @@
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 { promisify } from 'util';
13
+ import path from 'path';
14
+ import compression from 'compression';
15
+ import utils from './utils.js';
16
+ import RequestContext from './RequestContext.js';
17
+ import { asyncHandler, BaseServer } from './BaseServer.js';
18
+ import LiveReload from './LiveReload.js';
19
+
20
+ export class HelixServer extends BaseServer {
21
+ /**
22
+ * Creates a new HelixServer for the given project.
23
+ * @param {HelixProject} project
24
+ */
25
+ constructor(project) {
26
+ super(project);
27
+ this._liveReload = null;
28
+ this._enableLiveReload = false;
29
+ this._app.use(compression());
30
+ }
31
+
32
+ withLiveReload(value) {
33
+ this._enableLiveReload = value;
34
+ return this;
35
+ }
36
+
37
+ /**
38
+ * Proxy Mode route handler
39
+ * @param {Express.Request} req request
40
+ * @param {Express.Response} res response
41
+ */
42
+ async handleProxyModeRequest(req, res) {
43
+ const sendFile = promisify(res.sendFile).bind(res);
44
+ const ctx = new RequestContext(req, this._project);
45
+ const { log } = this;
46
+ const proxyUrl = new URL(this._project.proxyUrl);
47
+
48
+ const filePath = path.join(this._project.directory, ctx.path);
49
+ if (path.relative(this._project.directory, filePath).startsWith('..')) {
50
+ log.info(`refuse to serve file outside the project directory: ${filePath}`);
51
+ res.status(403).send('');
52
+ return;
53
+ }
54
+
55
+ const liveReload = this._liveReload;
56
+ if (liveReload) {
57
+ liveReload.startRequest(ctx.requestId, ctx.path);
58
+ }
59
+
60
+ // try to serve static
61
+ try {
62
+ log.debug('trying to serve local file', filePath);
63
+ await sendFile(filePath, {
64
+ dotfiles: 'allow',
65
+ headers: {
66
+ 'access-control-allow-origin': '*',
67
+ },
68
+ });
69
+ if (liveReload) {
70
+ liveReload.registerFile(ctx.requestId, filePath);
71
+ }
72
+ return;
73
+ } catch (e) {
74
+ log.debug(`Error while delivering resource ${ctx.path} - ${e.stack || e}`);
75
+ } finally {
76
+ if (liveReload) {
77
+ liveReload.endRequest(ctx.requestId);
78
+ }
79
+ }
80
+
81
+ // use proxy
82
+ try {
83
+ const url = new URL(ctx.url, proxyUrl);
84
+ for (const [key, value] of proxyUrl.searchParams.entries()) {
85
+ url.searchParams.append(key, value);
86
+ }
87
+ await utils.proxyRequest(ctx, url.href, req, res, {
88
+ injectLiveReload: this._project.liveReload,
89
+ headHtml: this._project.headHtml,
90
+ indexer: this._project.indexer,
91
+ cacheDirectory: this._project.cacheDirectory,
92
+ file404html: this._project.file404html,
93
+ });
94
+ } catch (err) {
95
+ log.error(`Failed to proxy AEM request ${ctx.path}: ${err.message}`);
96
+ res.status(502).send(`Failed to proxy AEM request: ${err.message}`);
97
+ }
98
+
99
+ this.emit('request', req, res, ctx);
100
+ }
101
+
102
+ async setupApp() {
103
+ await super.setupApp();
104
+ if (this._enableLiveReload) {
105
+ this._liveReload = new LiveReload(this.log);
106
+ await this._liveReload.init(this.app, this._server);
107
+ }
108
+ const handler = asyncHandler(this.handleProxyModeRequest.bind(this));
109
+ this.app.get('*', handler);
110
+ this.app.post('*', handler);
111
+ }
112
+
113
+ async doStop() {
114
+ await super.stop();
115
+ if (this._liveReload) {
116
+ await this._liveReload.stop();
117
+ delete this._liveReload;
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,152 @@
1
+ /*
2
+ * Copyright 2021 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 { resolve } from 'path';
13
+ import { IndexConfig } from '@adobe/helix-shared-config';
14
+ import { indexResource, contains } from '@adobe/helix-shared-indexer';
15
+ import chalk from 'chalk-template';
16
+ import chokidar from 'chokidar';
17
+
18
+ export default class Indexer {
19
+ constructor() {
20
+ this._cwd = process.cwd();
21
+ this._logger = null;
22
+ this._last = {};
23
+ this._index = null;
24
+ this._printIndex = false;
25
+ }
26
+
27
+ withCwd(cwd) {
28
+ this._cwd = cwd;
29
+ return this;
30
+ }
31
+
32
+ withLogger(logger) {
33
+ this._logger = logger;
34
+ return this;
35
+ }
36
+
37
+ withPrintIndex(value) {
38
+ this._printIndex = value;
39
+ return this;
40
+ }
41
+
42
+ get log() {
43
+ return this._logger;
44
+ }
45
+
46
+ async init() {
47
+ await this.loadIndex();
48
+ // eslint-disable-next-line no-underscore-dangle
49
+ this._watcher = chokidar.watch([resolve(this._cwd, this._index._name)], {
50
+ persistent: true,
51
+ ignoreInitial: true,
52
+ });
53
+
54
+ this._watcher.on('all', async () => {
55
+ await this.loadIndex();
56
+ await this.onIndexChanged();
57
+ });
58
+ return this;
59
+ }
60
+
61
+ async close() {
62
+ if (this._watcher) {
63
+ await this._watcher.close();
64
+ delete this._watcher;
65
+ }
66
+ }
67
+
68
+ async loadIndex() {
69
+ const { log } = this;
70
+ try {
71
+ this._index = await new IndexConfig()
72
+ .withDirectory(this._cwd)
73
+ .withLogger(log)
74
+ .init();
75
+ } catch (e) {
76
+ log.error(`Error in helix-query.yaml: ${e.message}`);
77
+ }
78
+ if (this._printIndex && this._index.indices.length === 0) {
79
+ log.warn(chalk`AEM CLI started with {gray --print-index} but no valid {gray helix-query.yaml} found.`);
80
+ }
81
+ }
82
+
83
+ async onData(url, response) {
84
+ const { pathname } = new URL(url);
85
+ this._last = {
86
+ pathname,
87
+ response,
88
+ };
89
+ if (this._printIndex) {
90
+ await this.dump();
91
+ }
92
+ }
93
+
94
+ async onIndexChanged() {
95
+ await this.loadIndex();
96
+ if (this._printIndex) {
97
+ await this.dump();
98
+ }
99
+ }
100
+
101
+ async dump() {
102
+ const { log } = this;
103
+ const {
104
+ pathname,
105
+ response,
106
+ } = this._last;
107
+ if (!pathname) {
108
+ log.debug('No last path recorded, dump skipped.');
109
+ return;
110
+ }
111
+ const records = await this.getRecords(pathname, response);
112
+ if (records) {
113
+ if (records.length) {
114
+ log.info(chalk`Index information for {blue ${pathname}}`);
115
+ } else {
116
+ log.info(chalk`No index information matches {blue ${pathname}}`);
117
+ }
118
+ for (const idx of records) {
119
+ log.info(chalk`Index: {yellow ${idx.name}}`);
120
+ const pad = Object.keys(idx.properties).reduce((p, c) => Math.max(p, c.length), 0);
121
+ Object.entries(idx.properties).forEach(([key, value]) => {
122
+ if (typeof value === 'number') {
123
+ // eslint-disable-next-line no-param-reassign
124
+ value = chalk`{yellow ${value}}`;
125
+ } else {
126
+ // eslint-disable-next-line no-param-reassign
127
+ value = JSON.stringify(value);
128
+ }
129
+ log.info(chalk` {gray ${key.padStart(pad)}}: ${value}`);
130
+ });
131
+ }
132
+ }
133
+ }
134
+
135
+ async getRecords(pathname, response) {
136
+ if (!this._index) {
137
+ return null;
138
+ }
139
+ return this._index.indices
140
+ .map((config) => {
141
+ if (contains(config, pathname)) {
142
+ return {
143
+ name: config.name,
144
+ properties: indexResource(pathname, response, config, this.log),
145
+ };
146
+ } else {
147
+ return null;
148
+ }
149
+ })
150
+ .filter((r) => !!r);
151
+ }
152
+ }