@adobe/aem-cli 16.13.2 → 16.14.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 +17 -0
- package/package.json +1 -1
- package/src/server/HelixProject.js +37 -1
- package/src/server/HelixServer.js +95 -0
- package/src/up.cmd.js +8 -1
- package/src/up.js +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
# [16.14.0](https://github.com/adobe/helix-cli/compare/v16.13.2...v16.14.0) (2025-09-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* address CodeQL security alert for HTML folder handler ([76450f2](https://github.com/adobe/helix-cli/commit/76450f25c33d0300adc53fe3b6e7696974e0e384))
|
|
7
|
+
* address PR review comments for HTML folder feature ([c8ff512](https://github.com/adobe/helix-cli/commit/c8ff512d97227e94eb8890e083c533db92c4c663))
|
|
8
|
+
* address PR review comments for HTML folder security ([842650e](https://github.com/adobe/helix-cli/commit/842650e96d421a32a276573b40dfa6b7a1b3b52c))
|
|
9
|
+
* make HTML folder 404 test more robust for CI environment ([9f5c605](https://github.com/adobe/helix-cli/commit/9f5c60546ca22faf611eedbb3735557ebaa83c0f))
|
|
10
|
+
* mock both URL patterns for HTML folder proxy test ([7be689a](https://github.com/adobe/helix-cli/commit/7be689a90c7d85b451ca6c1d00a1a67b53483d68))
|
|
11
|
+
* update test to handle proxy behavior correctly in CI ([52f8afa](https://github.com/adobe/helix-cli/commit/52f8afa6823f4bbc2e45ade57790c2cce722cf59))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* add --html-folder flag to serve HTML files without extensions ([682e8a2](https://github.com/adobe/helix-cli/commit/682e8a2dea8d835281a8d41eb0ef05e6c07d04b1))
|
|
17
|
+
|
|
1
18
|
## [16.13.2](https://github.com/adobe/helix-cli/compare/v16.13.1...v16.13.2) (2025-09-17)
|
|
2
19
|
|
|
3
20
|
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
|
-
import { resolve } from 'path';
|
|
12
|
+
import path, { resolve } from 'path';
|
|
13
13
|
import { lstat } from 'fs/promises';
|
|
14
14
|
import { HelixServer } from './HelixServer.js';
|
|
15
15
|
import { BaseProject } from './BaseProject.js';
|
|
@@ -78,6 +78,22 @@ export class HelixProject extends BaseProject {
|
|
|
78
78
|
return this;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
withHtmlFolder(value) {
|
|
82
|
+
if (value) {
|
|
83
|
+
// Security: reject any paths with traversal patterns or absolute paths
|
|
84
|
+
if (path.isAbsolute(value) || value.includes('..') || value.startsWith('/')) {
|
|
85
|
+
throw new Error(`Invalid HTML folder name: ${value} only folders within the current workspace are allowed`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this._htmlFolder = value;
|
|
89
|
+
this._server.withHtmlFolder(value);
|
|
90
|
+
} else {
|
|
91
|
+
this._htmlFolder = value;
|
|
92
|
+
this._server.withHtmlFolder(value);
|
|
93
|
+
}
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
get proxyUrl() {
|
|
82
98
|
return this._proxyUrl;
|
|
83
99
|
}
|
|
@@ -115,6 +131,10 @@ export class HelixProject extends BaseProject {
|
|
|
115
131
|
return this._headHtml;
|
|
116
132
|
}
|
|
117
133
|
|
|
134
|
+
get htmlFolder() {
|
|
135
|
+
return this._htmlFolder;
|
|
136
|
+
}
|
|
137
|
+
|
|
118
138
|
async init() {
|
|
119
139
|
await super.init();
|
|
120
140
|
this._indexer = new Indexer()
|
|
@@ -161,11 +181,27 @@ export class HelixProject extends BaseProject {
|
|
|
161
181
|
}
|
|
162
182
|
}
|
|
163
183
|
|
|
184
|
+
async initHtmlFolder() {
|
|
185
|
+
if (this._htmlFolder && this.liveReload) {
|
|
186
|
+
const htmlFolderPath = resolve(this.directory, this._htmlFolder);
|
|
187
|
+
try {
|
|
188
|
+
await lstat(htmlFolderPath);
|
|
189
|
+
this.log.debug(`Registered HTML folder for live-reload: ${this._htmlFolder}`);
|
|
190
|
+
// Watch all HTML files in the folder - only .html extension
|
|
191
|
+
this.liveReload.registerFiles([`${htmlFolderPath}/**/*.html`], `/${this._htmlFolder}/`);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
this.log.error(`HTML folder '${this._htmlFolder}' does not exist`);
|
|
194
|
+
throw new Error(`HTML folder '${this._htmlFolder}' does not exist`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
164
199
|
async start() {
|
|
165
200
|
this.log.debug('Launching AEM dev server...');
|
|
166
201
|
await super.start();
|
|
167
202
|
await this.initHeadHtml();
|
|
168
203
|
await this.init404Html();
|
|
204
|
+
await this.initHtmlFolder();
|
|
169
205
|
if (this._indexer) {
|
|
170
206
|
await this._indexer.init();
|
|
171
207
|
}
|
|
@@ -13,6 +13,7 @@ import crypto from 'crypto';
|
|
|
13
13
|
import express from 'express';
|
|
14
14
|
import { promisify } from 'util';
|
|
15
15
|
import path from 'path';
|
|
16
|
+
import { lstat } from 'fs/promises';
|
|
16
17
|
import compression from 'compression';
|
|
17
18
|
import utils from './utils.js';
|
|
18
19
|
import RequestContext from './RequestContext.js';
|
|
@@ -62,6 +63,12 @@ export class HelixServer extends BaseServer {
|
|
|
62
63
|
return this;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
withHtmlFolder(value) {
|
|
67
|
+
// It's now sanitized in HelixProject.withHtmlFolder
|
|
68
|
+
this._htmlFolder = value;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
async handleLogin(req, res) {
|
|
66
73
|
// disable autologin if login was called at least once
|
|
67
74
|
this._autoLogin = false;
|
|
@@ -147,6 +154,86 @@ export class HelixServer extends BaseServer {
|
|
|
147
154
|
.send('');
|
|
148
155
|
}
|
|
149
156
|
|
|
157
|
+
/**
|
|
158
|
+
* HTML Folder handler - serves HTML files without extensions
|
|
159
|
+
* @param {Express.Request} req request
|
|
160
|
+
* @param {Express.Response} res response
|
|
161
|
+
* @param {Function} next next middleware
|
|
162
|
+
*/
|
|
163
|
+
async handleHtmlFolderRequest(req, res, next) {
|
|
164
|
+
if (!this._htmlFolder) {
|
|
165
|
+
return next();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Use Express's req.path for pathname extraction
|
|
169
|
+
const pathname = req.path;
|
|
170
|
+
const folderPrefix = `/${this._htmlFolder}/`;
|
|
171
|
+
|
|
172
|
+
// Check if the request is for the HTML folder
|
|
173
|
+
if (!pathname.startsWith(folderPrefix)) {
|
|
174
|
+
return next();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Extract the path within the HTML folder
|
|
178
|
+
const relativePath = pathname.slice(folderPrefix.length);
|
|
179
|
+
|
|
180
|
+
// Security check: prevent path traversal with /../ anywhere in the path
|
|
181
|
+
if (relativePath.includes('/../')) {
|
|
182
|
+
return next();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Don't process if it already has an extension
|
|
186
|
+
if (relativePath.includes('.')) {
|
|
187
|
+
return next();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Build the HTML file path - only support .html extension
|
|
191
|
+
const htmlFile = path.join(this._project.directory, this._htmlFolder, `${relativePath}.html`);
|
|
192
|
+
|
|
193
|
+
// Security check: ensure the file is within the project directory
|
|
194
|
+
const relPath = path.relative(this._project.directory, htmlFile);
|
|
195
|
+
if (relPath.startsWith('..') || path.isAbsolute(relPath)) {
|
|
196
|
+
return next();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check if the HTML file exists and is a file
|
|
200
|
+
try {
|
|
201
|
+
const stats = await lstat(htmlFile);
|
|
202
|
+
if (!stats.isFile()) {
|
|
203
|
+
return next();
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
// File doesn't exist, continue to next handler
|
|
207
|
+
return next();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const sendFile = promisify(res.sendFile).bind(res);
|
|
211
|
+
const { log } = this;
|
|
212
|
+
const liveReload = this._liveReload;
|
|
213
|
+
|
|
214
|
+
// Register for live reload if enabled
|
|
215
|
+
if (liveReload) {
|
|
216
|
+
liveReload.startRequest(req.id, req.url);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Serve the file
|
|
220
|
+
await sendFile(htmlFile, {
|
|
221
|
+
dotfiles: 'deny',
|
|
222
|
+
headers: {
|
|
223
|
+
'access-control-allow-origin': '*',
|
|
224
|
+
'content-type': 'text/html; charset=utf-8',
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (liveReload) {
|
|
229
|
+
liveReload.registerFile(req.id, htmlFile);
|
|
230
|
+
liveReload.endRequest(req.id);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
log.debug(`served HTML file ${htmlFile} for ${req.url}`);
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
150
237
|
/**
|
|
151
238
|
* Proxy Mode route handler
|
|
152
239
|
* @param {Express.Request} req request
|
|
@@ -244,6 +331,14 @@ export class HelixServer extends BaseServer {
|
|
|
244
331
|
this.app.post(LOGIN_ACK_ROUTE, express.json(), asyncHandler(this.handleLoginAck.bind(this)));
|
|
245
332
|
this.app.options(LOGIN_ACK_ROUTE, asyncHandler(this.handleLoginAck.bind(this)));
|
|
246
333
|
|
|
334
|
+
// Add HTML folder handler before the general proxy handler
|
|
335
|
+
if (this._htmlFolder) {
|
|
336
|
+
// Only handle GET requests for the HTML folder path
|
|
337
|
+
const htmlFolderPattern = new RegExp(`^/${this._htmlFolder}/.*`);
|
|
338
|
+
this.app.get(htmlFolderPattern, asyncHandler(this.handleHtmlFolderRequest.bind(this)));
|
|
339
|
+
this.log.info(`Serving HTML files from folder: ${this._htmlFolder}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
247
342
|
const handler = asyncHandler(this.handleProxyModeRequest.bind(this));
|
|
248
343
|
this.app.get(/.*/, handler);
|
|
249
344
|
this.app.post(/.*/, handler);
|
package/src/up.cmd.js
CHANGED
|
@@ -54,6 +54,12 @@ export default class UpCommand extends AbstractServerCommand {
|
|
|
54
54
|
return this;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
withHtmlFolder(value) {
|
|
58
|
+
// Basic validation - detailed validation done in HelixProject
|
|
59
|
+
this._htmlFolder = value;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
57
63
|
async doStop() {
|
|
58
64
|
await super.doStop();
|
|
59
65
|
if (this._watcher) {
|
|
@@ -105,7 +111,8 @@ export default class UpCommand extends AbstractServerCommand {
|
|
|
105
111
|
.withPrintIndex(this._printIndex)
|
|
106
112
|
.withAllowInsecure(this._allowInsecure)
|
|
107
113
|
.withSiteToken(this._siteToken)
|
|
108
|
-
.withCookies(this._cookies)
|
|
114
|
+
.withCookies(this._cookies)
|
|
115
|
+
.withHtmlFolder(this._htmlFolder);
|
|
109
116
|
|
|
110
117
|
this.log.info(chalk`{yellow ___ ________ ___ __ __ v${pkgJson.version}}`);
|
|
111
118
|
this.log.info(chalk`{yellow / | / ____/ |/ / _____(_)___ ___ __ __/ /___ _/ /_____ _____}`);
|
package/src/up.js
CHANGED
|
@@ -115,6 +115,11 @@ export default function up() {
|
|
|
115
115
|
type: 'boolean',
|
|
116
116
|
default: false,
|
|
117
117
|
})
|
|
118
|
+
.option('html-folder', {
|
|
119
|
+
alias: 'htmlFolder',
|
|
120
|
+
describe: 'Serve HTML files from this folder without extensions (e.g., /folder/file serves folder/file.html) use this to preview content changes if you do not have access to the authoring system',
|
|
121
|
+
type: 'string',
|
|
122
|
+
})
|
|
118
123
|
|
|
119
124
|
.help();
|
|
120
125
|
},
|
|
@@ -141,6 +146,7 @@ export default function up() {
|
|
|
141
146
|
.withKill(argv.stopOther)
|
|
142
147
|
.withCache(argv.alphaCache)
|
|
143
148
|
.withCookies(argv.cookies)
|
|
149
|
+
.withHtmlFolder(argv.htmlFolder)
|
|
144
150
|
.run();
|
|
145
151
|
},
|
|
146
152
|
};
|