From cc9c4d95629a094bcafa2f351eaf8b86f2031340 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 30 Apr 2022 22:15:02 -0400 Subject: [PATCH 1/7] Fix source maps in HMR --- packages/core/integration-tests/test/hmr.js | 56 +++++++++++- .../core/integration-tests/test/server.js | 27 +----- packages/core/test-utils/src/utils.js | 34 ++++++++ packages/reporters/dev-server/package.json | 1 + .../reporters/dev-server/src/HMRServer.js | 48 ++++++++++- packages/reporters/dev-server/src/Server.js | 17 ++++ .../runtimes/hmr/src/loaders/hmr-runtime.js | 85 +++++++++++++++++-- 7 files changed, 233 insertions(+), 35 deletions(-) diff --git a/packages/core/integration-tests/test/hmr.js b/packages/core/integration-tests/test/hmr.js index 4818bd97400..a4a3f76529f 100644 --- a/packages/core/integration-tests/test/hmr.js +++ b/packages/core/integration-tests/test/hmr.js @@ -10,6 +10,7 @@ import { overlayFS, sleep, run, + request, } from '@parcel/test-utils'; import WebSocket from 'ws'; import json5 from 'json5'; @@ -110,6 +111,7 @@ describe('hmr', function () { return { outputs: JSON.parse(JSON.stringify(outputs)), reloaded, + bundleGraph, }; } @@ -157,10 +159,12 @@ describe('hmr', function () { assert.equal(message.type, 'update'); // Figure out why output doesn't change... - let localAsset = message.assets.find( - asset => asset.output === 'exports.a = 5;\nexports.b = 5;\n', + let localAsset = message.assets.find(asset => + asset.output.includes('exports.a = 5;\nexports.b = 5;\n'), ); assert(!!localAsset); + assert(localAsset.output.includes('//# sourceMappingURL')); + assert(localAsset.output.includes('//# sourceURL')); }); it('should emit an HMR update for all new dependencies along with the changed file', async function () { @@ -331,6 +335,31 @@ describe('hmr', function () { assert.equal(message.type, 'update'); }); + + it('should respond to requests for assets by id', async function () { + let port = await getPort(); + let b = bundler(path.join(__dirname, '/input/index.js'), { + serveOptions: {port}, + hmrOptions: {port}, + inputFS: overlayFS, + config, + }); + + subscription = await b.watch(); + let event = await getNextBuild(b); + + let bundleGraph = nullthrows(event.bundleGraph); + let asset = nullthrows(bundleGraph.getBundles()[0].getMainEntry()); + let contents = await request('/__parcel_hmr/' + asset.id, port); + let publicId = nullthrows(bundleGraph).getAssetPublicId(asset); + assert( + contents.startsWith( + `parcelHotUpdate['${publicId}'] = function (require, module, exports) {`, + ), + ); + assert(contents.includes('//# sourceMappingURL')); + assert(contents.includes('//# sourceURL')); + }); }); // TODO: add test for 4532 (`require` call in modified asset in child bundle where HMR runtime runs in parent bundle) @@ -548,6 +577,29 @@ module.hot.dispose((data) => { assert.notEqual(url.search, search); }); + it('should have correct source locations in errors', async function () { + let {outputs, bundleGraph} = await testHMRClient( + 'hmr-accept-self', + outputs => { + return { + 'local.js': 'output(new Error().stack);', + }; + }, + ); + + let asset = bundleGraph + .getBundles()[0] + .traverseAssets((asset, _, actions) => { + if (asset.filePath.endsWith('local.js')) { + actions.stop(); + return asset; + } + }); + + let stack = outputs.pop(); + assert(stack.includes('/__parcel_hmr/' + nullthrows(asset).id)); + }); + /* it.skip('should accept HMR updates in the runtime after an initial error', async function() { await fs.mkdirp(path.join(__dirname, '/input')); diff --git a/packages/core/integration-tests/test/server.js b/packages/core/integration-tests/test/server.js index 0c820c4ff7c..29ab7f8c26c 100644 --- a/packages/core/integration-tests/test/server.js +++ b/packages/core/integration-tests/test/server.js @@ -10,6 +10,7 @@ import { outputFS, overlayFS, ncp, + request as get, } from '@parcel/test-utils'; import http from 'http'; import https from 'https'; @@ -22,32 +23,6 @@ const config = path.join( './integration/custom-configs/.parcelrc-dev-server', ); -function get(file, port, client = http) { - return new Promise((resolve, reject) => { - // $FlowFixMe - client.get( - { - hostname: 'localhost', - port: port, - path: file, - rejectUnauthorized: false, - }, - res => { - res.setEncoding('utf8'); - let data = ''; - res.on('data', c => (data += c)); - res.on('end', () => { - if (res.statusCode !== 200) { - return reject({statusCode: res.statusCode, data}); - } - - resolve(data); - }); - }, - ); - }); -} - describe('server', function () { let subscription; diff --git a/packages/core/test-utils/src/utils.js b/packages/core/test-utils/src/utils.js index 3f9961615a8..bf273979da9 100644 --- a/packages/core/test-utils/src/utils.js +++ b/packages/core/test-utils/src/utils.js @@ -27,6 +27,8 @@ import nullthrows from 'nullthrows'; import {parser as postHtmlParse} from 'posthtml-parser'; import postHtml from 'posthtml'; import EventEmitter from 'events'; +import http from 'http'; +import https from 'https'; import {makeDeferredWithPromise, normalizeSeparators} from '@parcel/utils'; import _chalk from 'chalk'; @@ -734,6 +736,8 @@ function prepareBrowserContext( }, URL, Worker: createWorkerClass(bundle.filePath), + addEventListener() {}, + removeEventListener() {}, }, globals, ); @@ -1157,3 +1161,33 @@ export async function assertNoFilePathInCache( } } } + +export function request( + file: string, + port: number, + client: typeof http | typeof https = http, +): Promise { + return new Promise((resolve, reject) => { + // $FlowFixMe + client.get( + { + hostname: 'localhost', + port: port, + path: file, + rejectUnauthorized: false, + }, + res => { + res.setEncoding('utf8'); + let data = ''; + res.on('data', c => (data += c)); + res.on('end', () => { + if (res.statusCode !== 200) { + return reject({statusCode: res.statusCode, data}); + } + + resolve(data); + }); + }, + ); + }); +} diff --git a/packages/reporters/dev-server/package.json b/packages/reporters/dev-server/package.json index ca8d6859bb0..d2296e8139f 100644 --- a/packages/reporters/dev-server/package.json +++ b/packages/reporters/dev-server/package.json @@ -38,6 +38,7 @@ "connect": "^3.7.0", "ejs": "^3.1.6", "http-proxy-middleware": "^2.0.1", + "mime-types": "2.1.18", "nullthrows": "^1.1.1", "serve-handler": "^6.0.0", "ws": "^7.0.0" diff --git a/packages/reporters/dev-server/src/HMRServer.js b/packages/reporters/dev-server/src/HMRServer.js index 44a6e4196a9..9ce47592fb8 100644 --- a/packages/reporters/dev-server/src/HMRServer.js +++ b/packages/reporters/dev-server/src/HMRServer.js @@ -1,6 +1,13 @@ // @flow -import type {BuildSuccessEvent, Dependency, PluginOptions} from '@parcel/types'; +import type { + BuildSuccessEvent, + Dependency, + PluginOptions, + BundleGraph, + PackagedBundle, + Asset, +} from '@parcel/types'; import type {Diagnostic} from '@parcel/diagnostic'; import type {AnsiDiagnosticResult} from '@parcel/utils'; import type {ServerError, HMRServerOptions} from './types.js.flow'; @@ -8,9 +15,11 @@ import type {ServerError, HMRServerOptions} from './types.js.flow'; import WebSocket from 'ws'; import invariant from 'assert'; import {ansiHtml, prettyDiagnostic, PromiseQueue} from '@parcel/utils'; +import {HMR_ENDPOINT} from './Server'; export type HMRAsset = {| id: string, + url: string, type: string, output: string, envHash: string, @@ -141,9 +150,13 @@ export default class HMRServer { return { id: event.bundleGraph.getAssetPublicId(asset), + url: getSourceURL(event.bundleGraph, asset), type: asset.type, // No need to send the contents of non-JS assets to the client. - output: asset.type === 'js' ? await asset.getCode() : '', + output: + asset.type === 'js' + ? await getHotAssetContents(event.bundleGraph, asset) + : '', envHash: asset.env.id, depsByBundle, }; @@ -185,3 +198,34 @@ function getSpecifier(dep: Dependency): string { return dep.specifier; } + +export async function getHotAssetContents( + bundleGraph: BundleGraph, + asset: Asset, +): Promise { + let output = await asset.getCode(); + if (asset.type === 'js') { + let publicId = bundleGraph.getAssetPublicId(asset); + output = `parcelHotUpdate['${publicId}'] = function (require, module, exports) {${output}}`; + } + + let sourcemap = await asset.getMap(); + if (sourcemap) { + let sourcemapStringified = await sourcemap.stringify({ + format: 'inline', + sourceRoot: '/__parcel_source_root/', + // $FlowFixMe + fs: asset.fs, + }); + + invariant(typeof sourcemapStringified === 'string'); + output += `\n//# sourceMappingURL=${sourcemapStringified}`; + output += `\n//# sourceURL=${getSourceURL(bundleGraph, asset)}\n`; + } + + return output; +} + +function getSourceURL(bundleGraph, asset) { + return HMR_ENDPOINT + asset.id; +} diff --git a/packages/reporters/dev-server/src/Server.js b/packages/reporters/dev-server/src/Server.js index e09fcd3a6b5..57953224341 100644 --- a/packages/reporters/dev-server/src/Server.js +++ b/packages/reporters/dev-server/src/Server.js @@ -29,6 +29,9 @@ import connect from 'connect'; import serveHandler from 'serve-handler'; import {createProxyMiddleware} from 'http-proxy-middleware'; import {URL} from 'url'; +import {getHotAssetContents} from './HMRServer'; +import nullthrows from 'nullthrows'; +import mime from 'mime-types'; function setHeaders(res: Response) { res.setHeader('Access-Control-Allow-Origin', '*'); @@ -43,6 +46,7 @@ function setHeaders(res: Response) { } const SOURCES_ENDPOINT = '/__parcel_source_root'; +export const HMR_ENDPOINT = '/__parcel_hmr/'; const TEMPLATE_404 = fs.readFileSync( path.join(__dirname, 'templates/404.html'), 'utf8', @@ -132,6 +136,9 @@ export default class Server { if (this.errors) { return this.send500(req, res); + } else if (pathname.startsWith(HMR_ENDPOINT)) { + let id = pathname.slice(HMR_ENDPOINT.length); + return this.sendAsset(id, res); } else if (path.extname(pathname) === '') { // If the URL doesn't start with the public path, or the URL doesn't // have a file extension, send the main HTML bundle. @@ -240,6 +247,16 @@ export default class Server { } } + async sendAsset(id: string, res: Response) { + let bundleGraph = nullthrows(this.bundleGraph); + let asset = bundleGraph.getAssetById(id); + let output = await getHotAssetContents(bundleGraph, asset); + + setHeaders(res); + res.setHeader('Content-Type', mime.contentType(asset.type)); + res.end(output); + } + serveDist( req: Request, res: Response, diff --git a/packages/runtimes/hmr/src/loaders/hmr-runtime.js b/packages/runtimes/hmr/src/loaders/hmr-runtime.js index c51d5df8bf5..617f1be3987 100644 --- a/packages/runtimes/hmr/src/loaders/hmr-runtime.js +++ b/packages/runtimes/hmr/src/loaders/hmr-runtime.js @@ -92,8 +92,18 @@ if ((!parent || !parent.isParcelRequire) && typeof WebSocket !== 'undefined') { var ws = new WebSocket( protocol + '://' + hostname + (port ? ':' + port : '') + '/', ); + + // Safari doesn't support sourceURL in error stacks. + // eval may also be disabled via CSP, so do a quick check. + var supportsSourceURL = false; + try { + (0, eval)('throw new Error("test"); //# sourceURL=test.js'); + } catch (err) { + supportsSourceURL = err.stack.includes('test.js'); + } + // $FlowFixMe - ws.onmessage = function (event /*: {data: string, ...} */) { + ws.onmessage = async function (event /*: {data: string, ...} */) { checkedAssets = ({} /*: {|[string]: boolean|} */); acceptedAssets = ({} /*: {|[string]: boolean|} */); assetsToAccept = []; @@ -120,9 +130,15 @@ if ((!parent || !parent.isParcelRequire) && typeof WebSocket !== 'undefined') { if (handled) { console.clear(); - assets.forEach(function (asset) { - hmrApply(module.bundle.root, asset); - }); + // Dispatch custom event so other runtimes (e.g React Refresh) are aware. + if ( + typeof window !== 'undefined' && + typeof CustomEvent !== 'undefined' + ) { + window.dispatchEvent(new CustomEvent('parcelhmraccept')); + } + + await hmrApplyUpdates(assets); for (var i = 0; i < assetsToAccept.length; i++) { var id = assetsToAccept[i][1]; @@ -299,6 +315,59 @@ function reloadCSS() { }, 50); } +async function hmrApplyUpdates(assets) { + global.parcelHotUpdate = Object.create(null); + + let scriptsToRemove; + try { + // If sourceURL comments aren't supported in eval, we need to load + // the update from the dev server over HTTP so that stack traces + // are correct in errors/logs. This is much slower than eval, so + // we only do it if needed (currently just Safari). + // https://bugs.webkit.org/show_bug.cgi?id=137297 + // This path is also taken if a CSP disallows eval. + if (!supportsSourceURL) { + let promises = assets.map(asset => { + if (asset.type === 'js') { + if (typeof document !== 'undefined') { + let script = document.createElement('script'); + script.src = asset.url; + return new Promise((resolve, reject) => { + script.onload = () => resolve(script); + script.onerror = reject; + document.head?.appendChild(script); + }); + } else if (typeof importScripts === 'function') { + return new Promise((resolve, reject) => { + try { + importScripts(asset.url); + } catch (err) { + reject(err); + } + }); + } + } + }); + + scriptsToRemove = await Promise.all(promises); + } + + assets.forEach(function (asset) { + hmrApply(module.bundle.root, asset); + }); + } finally { + delete global.parcelHotUpdate; + + if (scriptsToRemove) { + scriptsToRemove.forEach(script => { + if (script) { + document.head?.removeChild(script); + } + }); + } + } +} + function hmrApply(bundle /*: ParcelRequire */, asset /*: HMRAsset */) { var modules = bundle.modules; if (!modules) { @@ -325,7 +394,13 @@ function hmrApply(bundle /*: ParcelRequire */, asset /*: HMRAsset */) { } } - var fn = new Function('require', 'module', 'exports', asset.output); + if (supportsSourceURL) { + // Global eval. We would use `new Function` here but browser + // support for source maps is better with eval. + (0, eval)(asset.output); + } + + let fn = global.parcelHotUpdate[asset.id]; modules[asset.id] = [fn, deps]; } else if (bundle.parent) { hmrApply(bundle.parent, asset); From e3756c583bda5ec0d96356958e78617774ce06be Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 30 Apr 2022 23:44:51 -0400 Subject: [PATCH 2/7] Add react-error-overlay --- packages/runtimes/react-refresh/package.json | 3 ++- .../react-refresh/src/ReactRefreshRuntime.js | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/runtimes/react-refresh/package.json b/packages/runtimes/react-refresh/package.json index c64fc7f9298..5c60b007fe3 100644 --- a/packages/runtimes/react-refresh/package.json +++ b/packages/runtimes/react-refresh/package.json @@ -22,6 +22,7 @@ "dependencies": { "@parcel/plugin": "2.5.0", "@parcel/utils": "2.5.0", - "react-refresh": "^0.9.0" + "react-refresh": "^0.9.0", + "react-error-overlay": "6.0.9" } } diff --git a/packages/runtimes/react-refresh/src/ReactRefreshRuntime.js b/packages/runtimes/react-refresh/src/ReactRefreshRuntime.js index 84586fd669f..7a80c3ba009 100644 --- a/packages/runtimes/react-refresh/src/ReactRefreshRuntime.js +++ b/packages/runtimes/react-refresh/src/ReactRefreshRuntime.js @@ -5,6 +5,7 @@ import {loadConfig} from '@parcel/utils'; const CODE = ` var Refresh = require('react-refresh/runtime'); +var ErrorOverlay = require('react-error-overlay'); Refresh.injectIntoGlobalHook(window); window.$RefreshReg$ = function() {}; @@ -12,7 +13,16 @@ window.$RefreshSig$ = function() { return function(type) { return type; }; -};`; +}; + +ErrorOverlay.startReportingRuntimeErrors({ + onError: function () {}, +}); + +window.addEventListener('parcelhmraccept', () => { + ErrorOverlay.dismissRuntimeErrors(); +}); +`; export default (new Runtime({ async apply({bundle, options}) { From b396f1a9b077a9f8c95583f1f9fe07e12479668b Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 30 Apr 2022 23:45:57 -0400 Subject: [PATCH 3/7] Add ability to launch editor from browser --- packages/core/utils/src/prettyDiagnostic.js | 21 +++++++++--- packages/reporters/dev-server/package.json | 1 + .../reporters/dev-server/src/HMRServer.js | 7 ++-- packages/reporters/dev-server/src/Server.js | 33 ++++++++++++++----- .../dev-server/src/templates/500.html | 4 ++- .../runtimes/hmr/src/loaders/hmr-runtime.js | 12 ++++++- .../react-refresh/src/ReactRefreshRuntime.js | 5 +++ 7 files changed, 67 insertions(+), 16 deletions(-) diff --git a/packages/core/utils/src/prettyDiagnostic.js b/packages/core/utils/src/prettyDiagnostic.js index 4168db380f6..5a349b5f7c8 100644 --- a/packages/core/utils/src/prettyDiagnostic.js +++ b/packages/core/utils/src/prettyDiagnostic.js @@ -10,10 +10,18 @@ import nullthrows from 'nullthrows'; // $FlowFixMe import terminalLink from 'terminal-link'; +export type FormattedCodeFrame = {| + location: string, + code: string, +|}; + export type AnsiDiagnosticResult = {| message: string, stack: string, + /** A formatted string containing all code frames, including their file locations. */ codeframe: string, + /** A list of code frames with highlighted code and file locations separately. */ + frames: Array, hints: Array, documentation: string, |}; @@ -39,6 +47,7 @@ export default async function prettyDiagnostic( (skipFormatting ? message : mdAnsi(message)), stack: '', codeframe: '', + frames: [], hints: [], documentation: '', }; @@ -69,16 +78,20 @@ export default async function prettyDiagnostic( }); } - result.codeframe += + let location = typeof filePath !== 'string' ? '' - : chalk.gray.underline( - `${filePath}:${highlights[0].start.line}:${highlights[0].start.column}\n`, - ); + : `${filePath}:${highlights[0].start.line}:${highlights[0].start.column}`; + result.codeframe += location ? chalk.gray.underline(location) + '\n' : ''; result.codeframe += formattedCodeFrame; if (codeFrame !== codeFrames[codeFrames.length - 1]) { result.codeframe += '\n\n'; } + + result.frames.push({ + location, + code: formattedCodeFrame, + }); } } diff --git a/packages/reporters/dev-server/package.json b/packages/reporters/dev-server/package.json index d2296e8139f..d61d5a43267 100644 --- a/packages/reporters/dev-server/package.json +++ b/packages/reporters/dev-server/package.json @@ -38,6 +38,7 @@ "connect": "^3.7.0", "ejs": "^3.1.6", "http-proxy-middleware": "^2.0.1", + "launch-editor": "^2.3.0", "mime-types": "2.1.18", "nullthrows": "^1.1.1", "serve-handler": "^6.0.0", diff --git a/packages/reporters/dev-server/src/HMRServer.js b/packages/reporters/dev-server/src/HMRServer.js index 9ce47592fb8..44f0195e716 100644 --- a/packages/reporters/dev-server/src/HMRServer.js +++ b/packages/reporters/dev-server/src/HMRServer.js @@ -35,7 +35,7 @@ export type HMRMessage = type: 'error', diagnostics: {| ansi: Array, - html: Array, + html: Array<$Rest>, |}, |}; @@ -90,7 +90,10 @@ export default class HMRServer { return { message: ansiHtml(d.message), stack: ansiHtml(d.stack), - codeframe: ansiHtml(d.codeframe), + frames: d.frames.map(f => ({ + location: f.location, + code: ansiHtml(f.code), + })), hints: d.hints.map(hint => ansiHtml(hint)), documentation: diagnostics[i].documentationURL ?? '', }; diff --git a/packages/reporters/dev-server/src/Server.js b/packages/reporters/dev-server/src/Server.js index 57953224341..3a2b851fe74 100644 --- a/packages/reporters/dev-server/src/Server.js +++ b/packages/reporters/dev-server/src/Server.js @@ -10,7 +10,7 @@ import type { } from '@parcel/types'; import type {Diagnostic} from '@parcel/diagnostic'; import type {FileSystem} from '@parcel/fs'; -import type {HTTPServer} from '@parcel/utils'; +import type {HTTPServer, FormattedCodeFrame} from '@parcel/utils'; import invariant from 'assert'; import path from 'path'; @@ -28,10 +28,11 @@ import ejs from 'ejs'; import connect from 'connect'; import serveHandler from 'serve-handler'; import {createProxyMiddleware} from 'http-proxy-middleware'; -import {URL} from 'url'; +import {URL, URLSearchParams} from 'url'; import {getHotAssetContents} from './HMRServer'; import nullthrows from 'nullthrows'; import mime from 'mime-types'; +import launchEditor from 'launch-editor'; function setHeaders(res: Response) { res.setHeader('Access-Control-Allow-Origin', '*'); @@ -47,6 +48,7 @@ function setHeaders(res: Response) { const SOURCES_ENDPOINT = '/__parcel_source_root'; export const HMR_ENDPOINT = '/__parcel_hmr/'; +const EDITOR_ENDPOINT = '/__parcel_launch_editor'; const TEMPLATE_404 = fs.readFileSync( path.join(__dirname, 'templates/404.html'), 'utf8', @@ -67,7 +69,8 @@ export default class Server { requestBundle: ?(bundle: PackagedBundle) => Promise; errors: Array<{| message: string, - stack: string, + stack: ?string, + frames: Array, hints: Array, documentation: string, |}> | null; @@ -117,9 +120,11 @@ export default class Server { return { message: ansiHtml(ansiDiagnostic.message), - stack: ansiDiagnostic.codeframe - ? ansiHtml(ansiDiagnostic.codeframe) - : ansiHtml(ansiDiagnostic.stack), + stack: ansiDiagnostic.stack ? ansiHtml(ansiDiagnostic.stack) : null, + frames: ansiDiagnostic.frames.map(f => ({ + location: f.location, + code: ansiHtml(f.code), + })), hints: ansiDiagnostic.hints.map(hint => ansiHtml(hint)), documentation: d.documentationURL ?? '', }; @@ -128,13 +133,25 @@ export default class Server { } respond(req: Request, res: Response): mixed { - let {pathname} = url.parse(req.originalUrl || req.url); + let {pathname, search} = url.parse(req.originalUrl || req.url); if (pathname == null) { pathname = '/'; } - if (this.errors) { + if (pathname.startsWith(EDITOR_ENDPOINT) && search) { + let query = new URLSearchParams(search); + let file = query.get('file'); + if (file) { + // File location might start with /__parcel_source_root if it came from a source map. + if (file.startsWith(SOURCES_ENDPOINT)) { + file = file.slice(SOURCES_ENDPOINT.length + 1); + } + launchEditor(file); + } + setHeaders(res); + res.end(); + } else if (this.errors) { return this.send500(req, res); } else if (pathname.startsWith(HMR_ENDPOINT)) { let id = pathname.slice(HMR_ENDPOINT.length); diff --git a/packages/reporters/dev-server/src/templates/500.html b/packages/reporters/dev-server/src/templates/500.html index 8b5975ba7e7..54ef4dbf9a9 100644 --- a/packages/reporters/dev-server/src/templates/500.html +++ b/packages/reporters/dev-server/src/templates/500.html @@ -72,7 +72,9 @@

🚨 Parcel encountered errors

<% errors.forEach(function(error){ %>

<%- error.message %>

-
<%- error.stack %>
+ +
<% if (error.frames?.length) { %><% error.frames.forEach(function(frame){ %><%- frame.location %> +<%- frame.code %><% }); %><% } else { %><%- error.stack %><% } %>
    <% error.hints.forEach(function(hint){ %>
  • <%- hint %>
  • diff --git a/packages/runtimes/hmr/src/loaders/hmr-runtime.js b/packages/runtimes/hmr/src/loaders/hmr-runtime.js index 617f1be3987..dec977b7dfc 100644 --- a/packages/runtimes/hmr/src/loaders/hmr-runtime.js +++ b/packages/runtimes/hmr/src/loaders/hmr-runtime.js @@ -214,7 +214,17 @@ function createErrorOverlay(diagnostics) { '
    '; for (let diagnostic of diagnostics) { - let stack = diagnostic.codeframe ? diagnostic.codeframe : diagnostic.stack; + let stack = diagnostic.frames.length + ? diagnostic.frames.reduce((p, frame) => { + return `${p} +${ + frame.location + } +${frame.code}`; + }, '') + : diagnostic.stack; errorHTML += `
    diff --git a/packages/runtimes/react-refresh/src/ReactRefreshRuntime.js b/packages/runtimes/react-refresh/src/ReactRefreshRuntime.js index 7a80c3ba009..ab1e203cd64 100644 --- a/packages/runtimes/react-refresh/src/ReactRefreshRuntime.js +++ b/packages/runtimes/react-refresh/src/ReactRefreshRuntime.js @@ -15,6 +15,11 @@ window.$RefreshSig$ = function() { }; }; +ErrorOverlay.setEditorHandler(function editorHandler(errorLocation) { + let file = \`\${errorLocation.fileName}:\${errorLocation.lineNumber ||}:\${errorLocation.colNumber || 1}\`; + fetch(\`/__parcel_launch_editor?file=\${encodeURIComponent(file)}\`); +}); + ErrorOverlay.startReportingRuntimeErrors({ onError: function () {}, }); From 1d34c7afef1dc64af84ce567fed6bc5f7460b416 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 30 Apr 2022 23:46:57 -0400 Subject: [PATCH 4/7] Automatically reload error page via HMR --- packages/reporters/dev-server/src/Server.js | 1 + .../dev-server/src/ServerReporter.js | 1 + .../dev-server/src/templates/500.html | 26 +++++++++++++++++++ .../reporters/dev-server/src/types.js.flow | 3 ++- 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/reporters/dev-server/src/Server.js b/packages/reporters/dev-server/src/Server.js index 3a2b851fe74..f65f3be5cfb 100644 --- a/packages/reporters/dev-server/src/Server.js +++ b/packages/reporters/dev-server/src/Server.js @@ -381,6 +381,7 @@ export default class Server { return res.end( ejs.render(TEMPLATE_500, { errors: this.errors, + hmrOptions: this.options.hmrOptions, }), ); } diff --git a/packages/reporters/dev-server/src/ServerReporter.js b/packages/reporters/dev-server/src/ServerReporter.js index c9e217b812a..5e85824d2bb 100644 --- a/packages/reporters/dev-server/src/ServerReporter.js +++ b/packages/reporters/dev-server/src/ServerReporter.js @@ -34,6 +34,7 @@ export default (new Reporter({ inputFS: options.inputFS, outputFS: options.outputFS, logger, + hmrOptions, }; server = new Server(serverOptions); diff --git a/packages/reporters/dev-server/src/templates/500.html b/packages/reporters/dev-server/src/templates/500.html index 54ef4dbf9a9..5abbde7abc3 100644 --- a/packages/reporters/dev-server/src/templates/500.html +++ b/packages/reporters/dev-server/src/templates/500.html @@ -84,5 +84,31 @@

    <%- error.message %>

    <% } %> <% }); %> + <% if (hmrOptions) { %> + + <% } %> diff --git a/packages/reporters/dev-server/src/types.js.flow b/packages/reporters/dev-server/src/types.js.flow index 81874f3e6f9..afcac1274f9 100644 --- a/packages/reporters/dev-server/src/types.js.flow +++ b/packages/reporters/dev-server/src/types.js.flow @@ -1,5 +1,5 @@ // @flow -import type {ServerOptions, PluginLogger} from '@parcel/types'; +import type {ServerOptions, PluginLogger, HMROptions} from '@parcel/types'; import type {FileSystem} from '@parcel/fs'; import type {HTTPServer} from '@parcel/utils'; import { @@ -27,6 +27,7 @@ export type DevServerOptions = {| inputFS: FileSystem, outputFS: FileSystem, logger: PluginLogger, + hmrOptions: ?HMROptions, |}; // TODO: Figure out if there is a node.js type that could be imported with a complete ServerError From 39b143d9de8a3c2a32e56d8edba9080e0c99174b Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 30 Apr 2022 23:47:25 -0400 Subject: [PATCH 5/7] Update yarn.lock --- yarn.lock | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/yarn.lock b/yarn.lock index d28916e8ec5..4b33d7c5490 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7986,6 +7986,14 @@ last-run@^1.1.0: default-resolution "^2.0.0" es6-weak-map "^2.0.1" +launch-editor@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.3.0.tgz#23b2081403b7eeaae2918bda510f3535ccab0ee4" + integrity sha512-3QrsCXejlWYHjBPFXTyGNhPj4rrQdB+5+r5r3wArpLH201aR+nWUgw/zKKkTmilCfY/sv6u8qo98pNvtg8LUTA== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.6.1" + lazystream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" @@ -10720,6 +10728,11 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-error-overlay@6.0.9: + version "6.0.9" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" + integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== + react-fast-compare@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" @@ -11520,6 +11533,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shell-quote@^1.6.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== + side-channel@^1.0.3, side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" From c75986f8a0597ab103c5fb940994d25f6f8dcf9f Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sat, 30 Apr 2022 23:59:36 -0400 Subject: [PATCH 6/7] lint --- packages/core/integration-tests/test/hmr.js | 2 +- packages/core/integration-tests/test/server.js | 1 - packages/runtimes/hmr/src/loaders/hmr-runtime.js | 2 +- packages/runtimes/react-refresh/src/ReactRefreshRuntime.js | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/core/integration-tests/test/hmr.js b/packages/core/integration-tests/test/hmr.js index a4a3f76529f..17ba5a10670 100644 --- a/packages/core/integration-tests/test/hmr.js +++ b/packages/core/integration-tests/test/hmr.js @@ -580,7 +580,7 @@ module.hot.dispose((data) => { it('should have correct source locations in errors', async function () { let {outputs, bundleGraph} = await testHMRClient( 'hmr-accept-self', - outputs => { + () => { return { 'local.js': 'output(new Error().stack);', }; diff --git a/packages/core/integration-tests/test/server.js b/packages/core/integration-tests/test/server.js index 29ab7f8c26c..563a81d8370 100644 --- a/packages/core/integration-tests/test/server.js +++ b/packages/core/integration-tests/test/server.js @@ -12,7 +12,6 @@ import { ncp, request as get, } from '@parcel/test-utils'; -import http from 'http'; import https from 'https'; import getPort from 'get-port'; import type {BuildEvent} from '@parcel/types'; diff --git a/packages/runtimes/hmr/src/loaders/hmr-runtime.js b/packages/runtimes/hmr/src/loaders/hmr-runtime.js index dec977b7dfc..933b3d31a2a 100644 --- a/packages/runtimes/hmr/src/loaders/hmr-runtime.js +++ b/packages/runtimes/hmr/src/loaders/hmr-runtime.js @@ -1,5 +1,5 @@ // @flow -/* global HMR_HOST, HMR_PORT, HMR_ENV_HASH, HMR_SECURE, chrome, browser */ +/* global HMR_HOST, HMR_PORT, HMR_ENV_HASH, HMR_SECURE, chrome, browser, importScripts */ /*:: import type { diff --git a/packages/runtimes/react-refresh/src/ReactRefreshRuntime.js b/packages/runtimes/react-refresh/src/ReactRefreshRuntime.js index ab1e203cd64..d9953766377 100644 --- a/packages/runtimes/react-refresh/src/ReactRefreshRuntime.js +++ b/packages/runtimes/react-refresh/src/ReactRefreshRuntime.js @@ -16,7 +16,7 @@ window.$RefreshSig$ = function() { }; ErrorOverlay.setEditorHandler(function editorHandler(errorLocation) { - let file = \`\${errorLocation.fileName}:\${errorLocation.lineNumber ||}:\${errorLocation.colNumber || 1}\`; + let file = \`\${errorLocation.fileName}:\${errorLocation.lineNumber || 1}:\${errorLocation.colNumber || 1}\`; fetch(\`/__parcel_launch_editor?file=\${encodeURIComponent(file)}\`); }); From 47c0b5dd25be300964566d67d9cba9938bc3e0ef Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sun, 1 May 2022 10:58:04 -0400 Subject: [PATCH 7/7] Remove .babelrc from hmr-runtime It relies on regeneratorRuntime which isn't always defined, and SWC will compile the runtime anyway --- packages/runtimes/hmr/src/loaders/.babelrc | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 packages/runtimes/hmr/src/loaders/.babelrc diff --git a/packages/runtimes/hmr/src/loaders/.babelrc b/packages/runtimes/hmr/src/loaders/.babelrc deleted file mode 100644 index b3a9d5d0a61..00000000000 --- a/packages/runtimes/hmr/src/loaders/.babelrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "presets": [ - ["@babel/preset-env", { - "targets": { - "ie": 11 - } - }] - ] -}