@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.
- package/CHANGELOG.md +58 -0
- package/CONTRIBUTING.md +14 -0
- package/README.md +35 -0
- package/eslint.config.js +2 -0
- package/package.json +8 -5
- package/packages/browser-injectables/README.md +66 -0
- package/packages/browser-injectables/eslint.config.js +76 -0
- package/packages/browser-injectables/package-lock.json +6990 -0
- package/packages/browser-injectables/package.json +26 -0
- package/packages/browser-injectables/scripts/copy-vendor.js +28 -0
- package/packages/browser-injectables/src/console-interceptor.js +81 -0
- package/packages/browser-injectables/web-test-runner.config.js +60 -0
- package/src/git-utils.js +124 -1
- package/src/server/HelixProject.js +5 -0
- package/src/server/HelixServer.js +11 -0
- package/src/server/LiveReload.js +40 -3
- package/src/server/utils.js +16 -0
- package/src/up.cmd.js +30 -4
- package/src/up.js +8 -1
|
@@ -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
|
-
|
|
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
|
|
package/src/server/LiveReload.js
CHANGED
|
@@ -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) => {
|
package/src/server/utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|