@adobe/aem-cli 16.11.3 → 16.13.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,26 @@
1
+ {
2
+ "name": "@adobe/aem-cli-browser-injectables",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Browser injectable scripts for AEM CLI development server",
7
+ "scripts": {
8
+ "lint": "eslint .",
9
+ "test": "web-test-runner",
10
+ "test:watch": "web-test-runner --watch",
11
+ "postinstall": "npm run copy-vendor && npx playwright install",
12
+ "copy-vendor": "node scripts/copy-vendor.js"
13
+ },
14
+ "devDependencies": {
15
+ "@adobe/eslint-config-helix": "^3.0.9",
16
+ "@eslint/config-helpers": "0.3.0",
17
+ "@esm-bundle/chai": "4.3.4-fix.0",
18
+ "@web/test-runner": "0.20.2",
19
+ "@web/test-runner-playwright": "0.11.1",
20
+ "@web/test-runner-junit-reporter": "0.8.0",
21
+ "@web/test-runner-mocha": "0.9.0"
22
+ },
23
+ "dependencies": {
24
+ "livereload-js": "4.0.2"
25
+ }
26
+ }
@@ -0,0 +1,28 @@
1
+ /*
2
+ * Copyright 2025 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
+
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ // Copy livereload.js from node_modules to vendor directory
20
+ const source = path.join(__dirname, '../node_modules/livereload-js/dist/livereload.js');
21
+ const dest = path.join(__dirname, '../vendor/livereload.js');
22
+
23
+ // Create vendor directory if it doesn't exist
24
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
25
+
26
+ // Copy the file
27
+ fs.copyFileSync(source, dest);
28
+ console.log('Copied livereload.js to vendor/');
@@ -0,0 +1,81 @@
1
+ /*
2
+ * Copyright 2025 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
+
13
+ /**
14
+ * Browser Console Interceptor
15
+ * Intercepts console methods and forwards them via WebSocket to the server
16
+ * This script is self-contained and browser-compatible (no module system)
17
+ */
18
+ (function iife() {
19
+ // Wait for LiveReload connection
20
+ var checkInterval = setInterval(function checkLiveReload() {
21
+ if (window.LiveReload && window.LiveReload.connector && window.LiveReload.connector.socket) {
22
+ clearInterval(checkInterval);
23
+
24
+ // Store original console methods
25
+ var originalConsole = {
26
+ log: console.log,
27
+ error: console.error,
28
+ warn: console.warn,
29
+ info: console.info,
30
+ };
31
+
32
+ // Helper to safely serialize arguments
33
+ function serializeArgs(args) {
34
+ return Array.from(args).map(function mapArg(arg) {
35
+ try {
36
+ if (arg instanceof Error) {
37
+ return { type: 'Error', message: arg.message, stack: arg.stack };
38
+ }
39
+ return JSON.parse(JSON.stringify(arg));
40
+ } catch (e) {
41
+ return String(arg);
42
+ }
43
+ });
44
+ }
45
+
46
+ // Get current file location
47
+ function getLocation() {
48
+ try {
49
+ var stack = new Error().stack;
50
+ var match = stack.match(/at.*?((https?:\/\/[^\s]+?):(\d+):(\d+))/);
51
+ if (match) {
52
+ return { url: match[2], line: match[3] };
53
+ }
54
+ } catch (e) {
55
+ // Ignore error when getting stack
56
+ }
57
+ return { url: window.location.href, line: 0 };
58
+ }
59
+
60
+ // Intercept console methods
61
+ ['log', 'error', 'warn', 'info'].forEach(function interceptLevel(level) {
62
+ console[level] = function interceptedConsole() {
63
+ // Call original method
64
+ originalConsole[level].apply(console, arguments);
65
+
66
+ // Forward to server if connected
67
+ if (window.LiveReload.connector.socket.readyState === 1) {
68
+ var location = getLocation();
69
+ window.LiveReload.connector.socket.send(JSON.stringify({
70
+ command: 'log',
71
+ level: level,
72
+ args: serializeArgs(arguments),
73
+ url: location.url,
74
+ line: location.line,
75
+ }));
76
+ }
77
+ };
78
+ });
79
+ }
80
+ }, 100);
81
+ }());
@@ -0,0 +1,60 @@
1
+ /*
2
+ * Copyright 2025 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
+
13
+ import { defaultReporter } from '@web/test-runner';
14
+ import { junitReporter } from '@web/test-runner-junit-reporter';
15
+ import { playwrightLauncher } from '@web/test-runner-playwright';
16
+
17
+ export default {
18
+ nodeResolve: true,
19
+ coverage: true,
20
+ reporters: [
21
+ defaultReporter(),
22
+ junitReporter({
23
+ outputPath: 'junit/browser-test-results.xml',
24
+ }),
25
+ ],
26
+ testFramework: {
27
+ type: 'mocha',
28
+ config: {
29
+ timeout: 20000, // 20 seconds for browser tests
30
+ },
31
+ },
32
+ browsers: [
33
+ playwrightLauncher({ product: 'chromium' }),
34
+ playwrightLauncher({ product: 'firefox' }),
35
+ playwrightLauncher({ product: 'webkit' }),
36
+ ],
37
+ coverageConfig: {
38
+ report: true,
39
+ reportDir: 'coverage-browser',
40
+ exclude: [
41
+ 'test/**',
42
+ 'node_modules/**',
43
+ '**/*.test.js',
44
+ 'scripts/**',
45
+ ],
46
+ },
47
+ files: [
48
+ 'test/**/*.test.html',
49
+ 'test/**/*.test.js',
50
+ ],
51
+ middleware: [
52
+ // Serve livereload.js from node_modules
53
+ async function serveLiveReload(context, next) {
54
+ if (context.url === '/__internal__/livereload.js') {
55
+ context.url = '/vendor/livereload.js';
56
+ }
57
+ await next();
58
+ },
59
+ ],
60
+ };
package/src/git-utils.js CHANGED
@@ -144,6 +144,24 @@ export default class GitUtils {
144
144
  * @returns {Promise<string>} current branch or tag
145
145
  */
146
146
  static async getBranch(dir, fallback = this.DEFAULT_BRANCH) {
147
+ // For worktrees, we need to get the actual git directory
148
+ const gitDir = await this.getGitDirectory(dir);
149
+
150
+ // If it's a worktree, try to get the branch from the worktree HEAD file
151
+ if (await this.isGitWorktree(dir)) {
152
+ try {
153
+ // Read the HEAD file in the worktree's git directory
154
+ const headPath = path.join(gitDir, 'HEAD');
155
+ const headContent = await fse.readFile(headPath, 'utf-8');
156
+ const match = headContent.match(/^ref: refs\/heads\/(.+)$/m);
157
+ if (match) {
158
+ return match[1].trim();
159
+ }
160
+ } catch (e) {
161
+ // Fall through to regular processing
162
+ }
163
+ }
164
+
147
165
  // current commit sha
148
166
  const rev = await git.resolveRef({ fs, dir, ref: 'HEAD' });
149
167
  // reverse-lookup tag from commit sha
@@ -194,7 +212,20 @@ export default class GitUtils {
194
212
  */
195
213
  static async getOrigin(dir) {
196
214
  try {
197
- const rmt = (await git.listRemotes({ fs, dir })).find((entry) => entry.remote === 'origin');
215
+ // For worktrees, we need to use the main repository's config
216
+ let gitDir = dir;
217
+ if (await this.isGitWorktree(dir)) {
218
+ // Get the worktree's git directory
219
+ const worktreeGitDir = await this.getGitDirectory(dir);
220
+ // Extract the main repository path from the worktree git dir
221
+ // Worktree git dirs are like: /path/to/repo/.git/worktrees/worktree-name
222
+ const match = worktreeGitDir.match(/^(.+?)\/\.git\/worktrees\//);
223
+ if (match) {
224
+ [, gitDir] = match;
225
+ }
226
+ }
227
+
228
+ const rmt = (await git.listRemotes({ fs, dir: gitDir })).find((entry) => entry.remote === 'origin');
198
229
  return typeof rmt === 'object' ? rmt.url : '';
199
230
  } catch (e) {
200
231
  // don't fail if directory is not a git repository
@@ -264,4 +295,96 @@ export default class GitUtils {
264
295
  }))
265
296
  .then((obj) => obj.object);
266
297
  }
298
+
299
+ /**
300
+ * Checks if the given directory is a git worktree.
301
+ *
302
+ * @param {string} dir working tree directory path
303
+ * @returns {Promise<boolean>} `true` if the directory is a git worktree
304
+ */
305
+ static async isGitWorktree(dir) {
306
+ const gitPath = path.resolve(dir, '.git');
307
+ try {
308
+ // Read the file directly - if it's not a file, readFile will throw
309
+ const content = await fse.readFile(gitPath, 'utf-8');
310
+ return content.includes('/worktrees/');
311
+ } catch (e) {
312
+ // Either doesn't exist, is a directory, or can't be read
313
+ // ignore
314
+ }
315
+ return false;
316
+ }
317
+
318
+ /**
319
+ * Checks if the given directory is a git submodule.
320
+ *
321
+ * @param {string} dir working tree directory path
322
+ * @returns {Promise<boolean>} `true` if the directory is a git submodule
323
+ */
324
+ static async isGitSubmodule(dir) {
325
+ const gitPath = path.resolve(dir, '.git');
326
+ try {
327
+ // Read the file directly - if it's not a file, readFile will throw
328
+ const content = await fse.readFile(gitPath, 'utf-8');
329
+ // Submodules have relative paths to .git/modules
330
+ return content.includes('/.git/modules/') || content.includes('\\.git\\modules\\');
331
+ } catch (e) {
332
+ // Either doesn't exist, is a directory, or can't be read
333
+ // ignore
334
+ }
335
+ return false;
336
+ }
337
+
338
+ /**
339
+ * Gets the actual git directory, resolving through worktree/submodule redirection.
340
+ *
341
+ * @param {string} dir working tree directory path
342
+ * @returns {Promise<string>} path to the actual git directory
343
+ */
344
+ static async getGitDirectory(dir) {
345
+ const gitPath = path.resolve(dir, '.git');
346
+ try {
347
+ // Try to read as a file first (worktree/submodule case)
348
+ const content = await fse.readFile(gitPath, 'utf-8');
349
+ const match = content.match(/^gitdir: (.+)$/m);
350
+ if (match) {
351
+ const targetPath = match[1].trim();
352
+ // If it's a relative path, resolve it relative to the directory
353
+ if (!path.isAbsolute(targetPath)) {
354
+ return path.resolve(dir, targetPath);
355
+ }
356
+ return targetPath;
357
+ }
358
+ } catch (e) {
359
+ // If readFile fails, it's likely a directory - check if it exists
360
+ try {
361
+ const stat = await fse.lstat(gitPath);
362
+ if (stat.isDirectory()) {
363
+ return gitPath;
364
+ }
365
+ } catch (statErr) {
366
+ // ignore
367
+ }
368
+ }
369
+ return gitPath;
370
+ }
371
+
372
+ /**
373
+ * Generates a deterministic port number based on branch name.
374
+ *
375
+ * @param {string} branchName the branch name to hash
376
+ * @param {number} basePort base port number (default: 3000)
377
+ * @param {number} range range of ports to use (default: 1000)
378
+ * @returns {number} port number between basePort and basePort + range - 1
379
+ */
380
+ static hashBranchToPort(branchName, basePort = 3000, range = 1000) {
381
+ let hash = 0;
382
+ for (let i = 0; i < branchName.length; i += 1) {
383
+ // eslint-disable-next-line no-bitwise
384
+ hash = ((hash << 5) - hash) + branchName.charCodeAt(i);
385
+ // eslint-disable-next-line no-bitwise
386
+ hash &= hash; // Convert to 32bit integer
387
+ }
388
+ return basePort + (Math.abs(hash) % range);
389
+ }
267
390
  }
@@ -32,6 +32,11 @@ export class HelixProject extends BaseProject {
32
32
  return this;
33
33
  }
34
34
 
35
+ withForwardBrowserLogs(value) {
36
+ this._server.withForwardBrowserLogs(value);
37
+ return this;
38
+ }
39
+
35
40
  withSiteToken(value) {
36
41
  this.siteToken = value;
37
42
  this._server.withSiteToken(value);
@@ -32,6 +32,7 @@ export class HelixServer extends BaseServer {
32
32
  super(project);
33
33
  this._liveReload = null;
34
34
  this._enableLiveReload = false;
35
+ this._forwardBrowserLogs = false;
35
36
  this._app.use(compression());
36
37
  this._autoLogin = true;
37
38
  this._cookies = false;
@@ -42,6 +43,15 @@ export class HelixServer extends BaseServer {
42
43
  return this;
43
44
  }
44
45
 
46
+ withForwardBrowserLogs(value) {
47
+ this._forwardBrowserLogs = value;
48
+ return this;
49
+ }
50
+
51
+ get forwardBrowserLogs() {
52
+ return this._forwardBrowserLogs;
53
+ }
54
+
45
55
  withSiteToken(value) {
46
56
  this._siteToken = value;
47
57
  return this;
@@ -225,6 +235,7 @@ export class HelixServer extends BaseServer {
225
235
  await super.setupApp();
226
236
  if (this._enableLiveReload) {
227
237
  this._liveReload = new LiveReload(this.log);
238
+ this._liveReload.withForwardBrowserLogs(this._forwardBrowserLogs);
228
239
  await this._liveReload.init(this.app, this._server);
229
240
  }
230
241
 
@@ -11,10 +11,10 @@
11
11
  */
12
12
  // eslint-disable-next-line max-classes-per-file
13
13
  import fs from 'fs';
14
+ import { createRequire } from 'module';
14
15
  import chokidar from 'chokidar';
15
16
  import WebSocket from 'faye-websocket';
16
17
  import { EventEmitter } from 'events';
17
- import { createRequire } from 'module';
18
18
 
19
19
  const require = createRequire(import.meta.url);
20
20
 
@@ -27,9 +27,10 @@ class ClientConnection extends EventEmitter {
27
27
  return `ws${ClientConnection.counter}`;
28
28
  }
29
29
 
30
- constructor(req, socket, head) {
30
+ constructor(req, socket, head, logger) {
31
31
  super();
32
32
  this.id = ClientConnection.nextId();
33
+ this.log = logger;
33
34
  this.ws = new WebSocket(req, socket, head);
34
35
  this.ws.onmessage = this._onMessage.bind(this);
35
36
  this.ws.onclose = this._onClose.bind(this);
@@ -47,6 +48,8 @@ class ClientConnection extends EventEmitter {
47
48
  return this._cmdHello(data);
48
49
  case 'info':
49
50
  return this._cmdInfo(data);
51
+ case 'log':
52
+ return this._cmdLog(data);
50
53
  default:
51
54
  return {};
52
55
  }
@@ -78,6 +81,30 @@ class ClientConnection extends EventEmitter {
78
81
  return { ...data || {}, id: this.id, url: this.url };
79
82
  }
80
83
 
84
+ _cmdLog(data) {
85
+ const {
86
+ level = 'log', args = [], url = 'unknown', line,
87
+ } = data;
88
+ const timestamp = new Date().toISOString();
89
+ const location = line ? `${url}:${line}` : url;
90
+
91
+ // Format browser logs distinctively
92
+ const prefix = `[Browser:${level}] ${timestamp} ${location}`;
93
+
94
+ // Serialize args safely
95
+ const message = args.map((arg) => {
96
+ try {
97
+ return typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg);
98
+ } catch (e) {
99
+ return '[Circular or Complex Object]';
100
+ }
101
+ }).join(' ');
102
+
103
+ // Use appropriate log level
104
+ const logMethod = this.log[level] || this.log.info;
105
+ logMethod(`${prefix} ${message}`);
106
+ }
107
+
81
108
  _send(data) {
82
109
  if (this.ws) {
83
110
  this.ws.send(JSON.stringify(data));
@@ -123,12 +150,22 @@ export default class LiveReload extends EventEmitter {
123
150
  // client connections
124
151
  this._connections = {};
125
152
  this._liveReloadJSPath = require.resolve('livereload-js/dist/livereload.js');
153
+ this._forwardBrowserLogs = false;
126
154
  }
127
155
 
128
156
  get log() {
129
157
  return this._logger;
130
158
  }
131
159
 
160
+ withForwardBrowserLogs(value) {
161
+ this._forwardBrowserLogs = value;
162
+ return this;
163
+ }
164
+
165
+ get forwardBrowserLogs() {
166
+ return this._forwardBrowserLogs;
167
+ }
168
+
132
169
  startRequest(requestId, pathname) {
133
170
  this._pending.set(requestId, {
134
171
  pathname,
@@ -211,7 +248,7 @@ export default class LiveReload extends EventEmitter {
211
248
  }
212
249
 
213
250
  _onSvrUpgrade(req, socket, head) {
214
- const cx = new ClientConnection(req, socket, head);
251
+ const cx = new ClientConnection(req, socket, head, this.log);
215
252
  this._connections[cx.id] = cx;
216
253
 
217
254
  socket.on('error', (e) => {
@@ -14,9 +14,19 @@ import crypto from 'crypto';
14
14
  import path from 'path';
15
15
  import { Socket } from 'net';
16
16
  import { PassThrough } from 'stream';
17
+ import { readFileSync } from 'fs';
18
+ import { fileURLToPath } from 'url';
17
19
  import cookie from 'cookie';
18
20
  import { getFetch } from '../fetch-utils.js';
19
21
 
22
+ // Load console interceptor script at startup
23
+ // eslint-disable-next-line no-underscore-dangle
24
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+ const CONSOLE_INTERCEPTOR = readFileSync(
26
+ path.join(__dirname, '../../packages/browser-injectables/src/console-interceptor.js'),
27
+ 'utf-8',
28
+ );
29
+
20
30
  const utils = {
21
31
  status2level(status, debug3xx) {
22
32
  if (status < 300) {
@@ -104,6 +114,12 @@ window.LiveReloadOptions = {
104
114
  newbody += `<script${nonce}>window.LiveReloadOptions={port:${server.port},host:location.hostname,https:${server.scheme === 'https'}};</script>`;
105
115
  }
106
116
  newbody += `<script${nonce} src="/__internal__/livereload.js"></script>`;
117
+
118
+ // Inject console interceptor if browser log forwarding is enabled
119
+ if (server.forwardBrowserLogs) {
120
+ newbody += `<script${nonce}>${CONSOLE_INTERCEPTOR}</script>`;
121
+ }
122
+
107
123
  newbody += body.substring(index);
108
124
  return newbody;
109
125
  }
package/src/up.cmd.js CHANGED
@@ -24,6 +24,11 @@ export default class UpCommand extends AbstractServerCommand {
24
24
  return this;
25
25
  }
26
26
 
27
+ withForwardBrowserLogs(value) {
28
+ this._forwardBrowserLogs = value;
29
+ return this;
30
+ }
31
+
27
32
  withUrl(value) {
28
33
  this._originalUrl = value;
29
34
  return this;
@@ -64,7 +69,15 @@ export default class UpCommand extends AbstractServerCommand {
64
69
  try {
65
70
  const stat = await fse.lstat(path.resolve(this.directory, '.git'));
66
71
  if (stat.isFile()) {
67
- throw Error('git submodules are not supported.');
72
+ // Check if it's a submodule or a worktree
73
+ if (await GitUtils.isGitSubmodule(this.directory)) {
74
+ throw Error('git submodules are not supported.');
75
+ }
76
+ // Verify it's actually a worktree
77
+ if (!await GitUtils.isGitWorktree(this.directory)) {
78
+ throw Error('Unsupported git configuration: .git is a file but not a valid worktree or submodule.');
79
+ }
80
+ // It's a worktree - this is allowed
68
81
  }
69
82
  } catch (e) {
70
83
  if (e.code === 'ENOENT') {
@@ -73,10 +86,20 @@ export default class UpCommand extends AbstractServerCommand {
73
86
  throw e;
74
87
  }
75
88
 
89
+ // Check if we're in a worktree and need to adjust the port
90
+ const isWorktree = await GitUtils.isGitWorktree(this.directory);
91
+ if (isWorktree && this._httpPort === 3000) {
92
+ // Only adjust port if using default port
93
+ const branch = await GitUtils.getBranch(this.directory);
94
+ this._httpPort = GitUtils.hashBranchToPort(branch);
95
+ this.log.info(chalk`Git worktree detected. Using port {cyan ${this._httpPort}} for branch {cyan ${branch}}`);
96
+ }
97
+
76
98
  // init dev default file params
77
99
  this._project = new HelixProject()
78
100
  .withCwd(this.directory)
79
101
  .withLiveReload(this._liveReload)
102
+ .withForwardBrowserLogs(this._forwardBrowserLogs)
80
103
  .withLogger(this._logger)
81
104
  .withKill(this._kill)
82
105
  .withPrintIndex(this._printIndex)
@@ -113,7 +136,7 @@ export default class UpCommand extends AbstractServerCommand {
113
136
 
114
137
  try {
115
138
  await this._project.init();
116
- this.watchGit();
139
+ await this.watchGit();
117
140
  } catch (e) {
118
141
  throw Error(`Unable to start AEM: ${e.message}`);
119
142
  }
@@ -155,10 +178,13 @@ export default class UpCommand extends AbstractServerCommand {
155
178
  /**
156
179
  * Watches the git repository for changes and restarts the server if necessary.
157
180
  */
158
- watchGit() {
181
+ async watchGit() {
159
182
  let timer = null;
160
183
 
161
- this._watcher = chokidar.watch(path.resolve(this._project.directory, '.git'), {
184
+ // Resolve the actual git directory for worktrees
185
+ const gitDir = await GitUtils.getGitDirectory(this._project.directory);
186
+
187
+ this._watcher = chokidar.watch(gitDir, {
162
188
  persistent: true,
163
189
  ignoreInitial: true,
164
190
  });
package/src/up.js CHANGED
@@ -97,7 +97,7 @@ export default function up() {
97
97
  describe: 'Path to local folder to cache the responses (note: this is an alpha feature, it may be removed without notice)',
98
98
  type: 'string',
99
99
  })
100
- .group(['url', 'livereload', 'no-livereload', 'open', 'no-open', 'print-index', 'cache'], 'AEM Options')
100
+ .group(['url', 'livereload', 'no-livereload', 'open', 'no-open', 'print-index', 'cache', 'forward-browser-logs'], 'AEM Options')
101
101
  .option('allow-insecure', {
102
102
  alias: 'allowInsecure',
103
103
  describe: 'Whether to allow insecure requests to the server',
@@ -109,6 +109,12 @@ export default function up() {
109
109
  type: 'boolean',
110
110
  default: false,
111
111
  })
112
+ .option('forward-browser-logs', {
113
+ alias: 'forwardBrowserLogs',
114
+ describe: 'Forward browser console logs to terminal',
115
+ type: 'boolean',
116
+ default: false,
117
+ })
112
118
 
113
119
  .help();
114
120
  },
@@ -127,6 +133,7 @@ export default function up() {
127
133
  .withOpen(path.basename(argv.$0) === 'aem' ? argv.open : false)
128
134
  .withTLS(argv.tlsKey, argv.tlsCert)
129
135
  .withLiveReload(argv.livereload)
136
+ .withForwardBrowserLogs(argv.forwardBrowserLogs)
130
137
  .withSiteToken(argv.siteToken || await getSiteTokenFromFile())
131
138
  .withUrl(argv.url)
132
139
  .withPrintIndex(argv.printIndex)