@adobe/aem-cli 16.12.0 → 16.13.1

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
+ };
@@ -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;
@@ -94,6 +99,7 @@ export default class UpCommand extends AbstractServerCommand {
94
99
  this._project = new HelixProject()
95
100
  .withCwd(this.directory)
96
101
  .withLiveReload(this._liveReload)
102
+ .withForwardBrowserLogs(this._forwardBrowserLogs)
97
103
  .withLogger(this._logger)
98
104
  .withKill(this._kill)
99
105
  .withPrintIndex(this._printIndex)
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)