diff --git a/lib/internal/loader/Loader.js b/lib/internal/loader/Loader.js index b8fe9ccfc614b8..492683c7fbd6dd 100644 --- a/lib/internal/loader/Loader.js +++ b/lib/internal/loader/Loader.js @@ -63,6 +63,12 @@ class Loader { // an object with the same keys as `exports`, whose values are get/set // functions for the actual exported values. this.dynamicInstantiate = undefined; + // Signals that Inspector should break when the first module is loaded + this.inspectorBreak = false; + } + + setInspectorBreak() { + this.inspectorBreak = true; } hook({ resolve = ModuleRequest.resolve, dynamicInstantiate }) { @@ -129,7 +135,13 @@ class Loader { } else { loaderInstance = ModuleRequest.loaders.get(format); } - job = new ModuleJob(this, url, loaderInstance); + let wrapper = null; + if (this.inspectorBreak) { + this.inspectorBreak = false; + wrapper = process.binding('inspector').callAndPauseOnStart; + } + + job = new ModuleJob(this, url, loaderInstance, wrapper); this.moduleMap.set(url, job); } return job; diff --git a/lib/internal/loader/ModuleJob.js b/lib/internal/loader/ModuleJob.js index a17c501609a426..d2908f168fe0ab 100644 --- a/lib/internal/loader/ModuleJob.js +++ b/lib/internal/loader/ModuleJob.js @@ -13,10 +13,12 @@ const enableDebug = (process.env.NODE_DEBUG || '').match(/\besm\b/) || class ModuleJob { // `loader` is the Loader instance used for loading dependencies. // `moduleProvider` is a function - constructor(loader, url, moduleProvider) { + // `initWrapper` is a function that wraps module initialization + constructor(loader, url, moduleProvider, initWrapper) { this.loader = loader; this.error = null; this.hadError = false; + this.initWrapper = initWrapper; // This is a Promise<{ module, reflect }>, whose fields will be copied // onto `this` by `link()` below once it has been resolved. @@ -81,7 +83,12 @@ class ModuleJob { } throw e; } - this.module.instantiate(); + if (this.initWrapper) { + this.initWrapper(this.module.instantiate, this.module); + this.initWrapper = null; + } else { + this.module.instantiate(); + } for (const dependencyJob of jobsInGraph) { // Calling `this.module.instantiate()` instantiates not only the // ModuleWrap in this module, but all modules in the graph. diff --git a/lib/module.js b/lib/module.js index 4c4ceaf847fba2..c77d50ab83d7d5 100644 --- a/lib/module.js +++ b/lib/module.js @@ -471,6 +471,10 @@ Module._load = function(request, parent, isMain) { ESMLoader = new Loader(); ESMLoader.hook(hooks); } + if (process._breakFirstLine) { + delete process._breakFirstLine; + ESMLoader.setInspectorBreak(); + } } Loader.registerImportDynamicallyCallback(ESMLoader); await ESMLoader.import(getURLFromFilePath(request).pathname); diff --git a/test/common/inspector-helper.js b/test/common/inspector-helper.js index 8fc778555d0454..2caedc6dfcd17c 100644 --- a/test/common/inspector-helper.js +++ b/test/common/inspector-helper.js @@ -5,12 +5,24 @@ const fs = require('fs'); const http = require('http'); const fixtures = require('../common/fixtures'); const { spawn } = require('child_process'); -const url = require('url'); +const { URL, parse: parseURL } = require('url'); const _MAINSCRIPT = fixtures.path('loop.js'); const DEBUG = false; const TIMEOUT = common.platformTimeout(15 * 1000); +function normalizeToFileUrl(path) { + const url = new URL(path, 'file:'); + if (url.protocol === 'file:') + return url.toString(); + // Windows workaround - URL consider 'drive:' (e.g. 'c:'') to be a protocol + return new URL(`/${path}`, 'file:').toString(); +} + +function sameScriptPath(script1, script2) { + return normalizeToFileUrl(script1) === normalizeToFileUrl(script2); +} + function spawnChildProcess(inspectorFlags, scriptContents, scriptFile) { const args = [].concat(inspectorFlags); if (scriptContents) { @@ -169,9 +181,8 @@ class InspectorSession { if (message.method === 'Debugger.scriptParsed') { const script = message['params']; const scriptId = script['scriptId']; - const url = script['url']; - this._scriptsIdsByUrl.set(scriptId, url); - if (url === _MAINSCRIPT) + this._scriptsIdsByUrl.set(scriptId, script['url']); + if (sameScriptPath(script['url'], this.scriptURL())) this.mainScriptId = scriptId; } @@ -238,11 +249,13 @@ class InspectorSession { return notification; } - _isBreakOnLineNotification(message, line, url) { + _isBreakOnLineNotification(message, line, expectedScriptPath) { if ('Debugger.paused' === message['method']) { const callFrame = message['params']['callFrames'][0]; const location = callFrame['location']; - assert.strictEqual(url, this._scriptsIdsByUrl.get(location['scriptId'])); + const scriptPath = this._scriptsIdsByUrl.get(location['scriptId']); + assert(sameScriptPath(scriptPath, expectedScriptPath), + `${scriptPath} !== ${expectedScriptPath}`); assert.strictEqual(line, location['lineNumber']); return true; } @@ -291,12 +304,26 @@ class InspectorSession { 'Waiting for the debugger to disconnect...'); await this.disconnect(); } + + scriptPath() { + return this._instance.scriptPath(); + } + + script() { + return this._instance.script(); + } + + scriptURL() { + return new URL(this.scriptPath(), 'file:').toString(); + } } class NodeInstance { constructor(inspectorFlags = ['--inspect-brk=0'], scriptContents = '', scriptFile = _MAINSCRIPT) { + this._scriptPath = scriptFile; + this._script = scriptFile ? null : scriptContents; this._portCallback = null; this.portPromise = new Promise((resolve) => this._portCallback = resolve); this._process = spawnChildProcess(inspectorFlags, scriptContents, @@ -373,7 +400,7 @@ class NodeInstance { return this.portPromise.then((port) => new Promise((resolve) => { http.get({ port, - path: url.parse(devtoolsUrl).path, + path: parseURL(devtoolsUrl).path, headers: { 'Connection': 'Upgrade', 'Upgrade': 'websocket', @@ -406,10 +433,16 @@ class NodeInstance { kill() { this._process.kill(); } -} -function readMainScriptSource() { - return fs.readFileSync(_MAINSCRIPT, 'utf8'); + scriptPath() { + return this._scriptPath; + } + + script() { + if (this._script === null) + this._script = fs.readFileSync(this.scriptPath(), 'utf8'); + return this._script; + } } function onResolvedOrRejected(promise, callback) { @@ -450,7 +483,5 @@ function fires(promise, error, timeoutMs) { } module.exports = { - mainScriptPath: _MAINSCRIPT, - readMainScriptSource, NodeInstance }; diff --git a/test/fixtures/loop.mjs b/test/fixtures/loop.mjs new file mode 100644 index 00000000000000..461fb393583e68 --- /dev/null +++ b/test/fixtures/loop.mjs @@ -0,0 +1,10 @@ +var t = 1; +var k = 1; +console.log('A message', 5); +while (t > 0) { + if (t++ === 1000) { + t = 0; + console.log(`Outputed message #${k++}`); + } +} +process.exit(55); diff --git a/test/parallel/test-inspector-esm.js b/test/parallel/test-inspector-esm.js new file mode 100644 index 00000000000000..fe0082ee9e0567 --- /dev/null +++ b/test/parallel/test-inspector-esm.js @@ -0,0 +1,119 @@ +'use strict'; +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const { NodeInstance } = require('../common/inspector-helper.js'); + +function assertNoUrlsWhileConnected(response) { + assert.strictEqual(response.length, 1); + assert.ok(!response[0].hasOwnProperty('devtoolsFrontendUrl')); + assert.ok(!response[0].hasOwnProperty('webSocketDebuggerUrl')); +} + +function assertScopeValues({ result }, expected) { + const unmatched = new Set(Object.keys(expected)); + for (const actual of result) { + const value = expected[actual['name']]; + assert.strictEqual(actual['value']['value'], value); + unmatched.delete(actual['name']); + } + assert.deepStrictEqual(Array.from(unmatched.values()), []); +} + +async function testBreakpointOnStart(session) { + console.log('[test]', + 'Verifying debugger stops on start (--inspect-brk option)'); + const commands = [ + { 'method': 'Runtime.enable' }, + { 'method': 'Debugger.enable' }, + { 'method': 'Debugger.setPauseOnExceptions', + 'params': { 'state': 'none' } }, + { 'method': 'Debugger.setAsyncCallStackDepth', + 'params': { 'maxDepth': 0 } }, + { 'method': 'Profiler.enable' }, + { 'method': 'Profiler.setSamplingInterval', + 'params': { 'interval': 100 } }, + { 'method': 'Debugger.setBlackboxPatterns', + 'params': { 'patterns': [] } }, + { 'method': 'Runtime.runIfWaitingForDebugger' } + ]; + + await session.send(commands); + await session.waitForBreakOnLine(0, session.scriptURL()); +} + +async function testBreakpoint(session) { + console.log('[test]', 'Setting a breakpoint and verifying it is hit'); + const commands = [ + { 'method': 'Debugger.setBreakpointByUrl', + 'params': { 'lineNumber': 5, + 'url': session.scriptURL(), + 'columnNumber': 0, + 'condition': '' + } + }, + { 'method': 'Debugger.resume' }, + ]; + await session.send(commands); + const { scriptSource } = await session.send({ + 'method': 'Debugger.getScriptSource', + 'params': { 'scriptId': session.mainScriptId } }); + assert(scriptSource && (scriptSource.includes(session.script())), + `Script source is wrong: ${scriptSource}`); + + await session.waitForConsoleOutput('log', ['A message', 5]); + const paused = await session.waitForBreakOnLine(5, session.scriptURL()); + const scopeId = paused.params.callFrames[0].scopeChain[0].object.objectId; + + console.log('[test]', 'Verify we can read current application state'); + const response = await session.send({ + 'method': 'Runtime.getProperties', + 'params': { + 'objectId': scopeId, + 'ownProperties': false, + 'accessorPropertiesOnly': false, + 'generatePreview': true + } + }); + assertScopeValues(response, { t: 1001, k: 1 }); + + let { result } = await session.send({ + 'method': 'Debugger.evaluateOnCallFrame', 'params': { + 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'expression': 'k + t', + 'objectGroup': 'console', + 'includeCommandLineAPI': true, + 'silent': false, + 'returnByValue': false, + 'generatePreview': true + } + }); + + assert.strictEqual(result['value'], 1002); + + result = (await session.send({ + 'method': 'Runtime.evaluate', 'params': { + 'expression': '5 * 5' + } + })).result; + assert.strictEqual(result['value'], 25); +} + +async function runTest() { + const child = new NodeInstance(['--inspect-brk=0', '--experimental-modules'], + '', fixtures.path('loop.mjs')); + + const session = await child.connectInspectorSession(); + assertNoUrlsWhileConnected(await child.httpGet(null, '/json/list')); + await testBreakpointOnStart(session); + await testBreakpoint(session); + await session.runToCompletion(); + assert.strictEqual((await child.expectShutdown()).exitCode, 55); +} + +common.crashOnUnhandledRejection(); + +runTest(); diff --git a/test/sequential/test-inspector-debug-brk-flag.js b/test/sequential/test-inspector-debug-brk-flag.js index 235e7043d80f45..0f193f4b875dc2 100644 --- a/test/sequential/test-inspector-debug-brk-flag.js +++ b/test/sequential/test-inspector-debug-brk-flag.js @@ -4,8 +4,7 @@ const common = require('../common'); common.skipIfInspectorDisabled(); const assert = require('assert'); -const { mainScriptPath, - NodeInstance } = require('../common/inspector-helper.js'); +const { NodeInstance } = require('../common/inspector-helper.js'); async function testBreakpointOnStart(session) { const commands = [ @@ -24,7 +23,7 @@ async function testBreakpointOnStart(session) { ]; session.send(commands); - await session.waitForBreakOnLine(0, mainScriptPath); + await session.waitForBreakOnLine(0, session.scriptPath()); } async function runTests() { diff --git a/test/sequential/test-inspector.js b/test/sequential/test-inspector.js index 992a12e90229ed..718ec94053d625 100644 --- a/test/sequential/test-inspector.js +++ b/test/sequential/test-inspector.js @@ -4,9 +4,7 @@ const common = require('../common'); common.skipIfInspectorDisabled(); const assert = require('assert'); -const { mainScriptPath, - readMainScriptSource, - NodeInstance } = require('../common/inspector-helper.js'); +const { NodeInstance } = require('../common/inspector-helper.js'); function checkListResponse(response) { assert.strictEqual(1, response.length); @@ -75,7 +73,7 @@ async function testBreakpointOnStart(session) { ]; await session.send(commands); - await session.waitForBreakOnLine(0, mainScriptPath); + await session.waitForBreakOnLine(0, session.scriptPath()); } async function testBreakpoint(session) { @@ -83,7 +81,7 @@ async function testBreakpoint(session) { const commands = [ { 'method': 'Debugger.setBreakpointByUrl', 'params': { 'lineNumber': 5, - 'url': mainScriptPath, + 'url': session.scriptPath(), 'columnNumber': 0, 'condition': '' } @@ -94,11 +92,11 @@ async function testBreakpoint(session) { const { scriptSource } = await session.send({ 'method': 'Debugger.getScriptSource', 'params': { 'scriptId': session.mainScriptId } }); - assert(scriptSource && (scriptSource.includes(readMainScriptSource())), + assert(scriptSource && (scriptSource.includes(session.script())), `Script source is wrong: ${scriptSource}`); await session.waitForConsoleOutput('log', ['A message', 5]); - const paused = await session.waitForBreakOnLine(5, mainScriptPath); + const paused = await session.waitForBreakOnLine(5, session.scriptPath()); const scopeId = paused.params.callFrames[0].scopeChain[0].object.objectId; console.log('[test]', 'Verify we can read current application state');