Skip to content

Commit

Permalink
Add manifest V3 true HMR (#8145)
Browse files Browse the repository at this point in the history
  • Loading branch information
101arrowz committed Jun 13, 2022
1 parent bc0d667 commit 52296ad
Show file tree
Hide file tree
Showing 11 changed files with 311 additions and 158 deletions.
26 changes: 19 additions & 7 deletions packages/core/utils/src/http-server.js
@@ -1,7 +1,15 @@
// @flow strict-local

import type {Server as HTTPOnlyServer} from 'http';
import type {Server as HTTPSServer} from 'https';
import type {
Server as HTTPOnlyServer,
IncomingMessage as HTTPRequest,
ServerResponse as HTTPResponse,
} from 'http';
import type {
Server as HTTPSServer,
IncomingMessage as HTTPSRequest,
ServerResponse as HTTPSResponse,
} from 'https';
import type {Socket} from 'net';
import type {FilePath, HTTPSOptions} from '@parcel/types';
import type {FileSystem} from '@parcel/fs';
Expand All @@ -12,12 +20,16 @@ import nullthrows from 'nullthrows';
import {getCertificate, generateCertificate} from './';

type CreateHTTPServerOpts = {|
https: ?(HTTPSOptions | boolean),
inputFS: FileSystem,
outputFS: FileSystem,
cacheDir: FilePath,
listener?: (mixed, mixed) => void,
listener?: (HTTPRequest | HTTPSRequest, HTTPResponse | HTTPSResponse) => void,
host?: string,
...
| {|
https: ?(HTTPSOptions | boolean),
inputFS: FileSystem,
outputFS: FileSystem,
cacheDir: FilePath,
|}
| {||},
|};

export type HTTPServer = HTTPOnlyServer | HTTPSServer;
Expand Down
11 changes: 7 additions & 4 deletions packages/packagers/webextension/src/WebExtensionPackager.js
Expand Up @@ -6,7 +6,7 @@ import {Packager} from '@parcel/plugin';
import {replaceURLReferences, relativeBundlePath} from '@parcel/utils';

export default (new Packager({
async package({bundle, bundleGraph}) {
async package({bundle, bundleGraph, options}) {
let assets = [];
bundle.traverseAssets(asset => {
assets.push(asset);
Expand Down Expand Up @@ -73,14 +73,17 @@ export default (new Packager({
}
}

if (manifest.manifest_version == 3 && options.hmrOptions) {
war.push({matches: ['<all_urls>'], resources: ['__parcel_hmr_proxy__']});
}

const warResult = (manifest.web_accessible_resources || []).concat(
manifest.manifest_version == 2
? [...new Set(war.flatMap(entry => entry.resources))]
: war,
);
if (warResult.length > 0) {
manifest.web_accessible_resources = warResult;
}

if (warResult.length > 0) manifest.web_accessible_resources = warResult;

let {contents} = replaceURLReferences({
bundle,
Expand Down
147 changes: 98 additions & 49 deletions packages/reporters/dev-server/src/HMRServer.js
Expand Up @@ -10,19 +10,33 @@ import type {
} from '@parcel/types';
import type {Diagnostic} from '@parcel/diagnostic';
import type {AnsiDiagnosticResult} from '@parcel/utils';
import type {ServerError, HMRServerOptions} from './types.js.flow';

import type {
ServerError,
HMRServerOptions,
Request,
Response,
} from './types.js.flow';
import {setHeaders, SOURCES_ENDPOINT} from './Server';

import nullthrows from 'nullthrows';
import url from 'url';
import mime from 'mime-types';
import WebSocket from 'ws';
import invariant from 'assert';
import {ansiHtml, prettyDiagnostic, PromiseQueue} from '@parcel/utils';
import {HMR_ENDPOINT} from './Server';
import {
ansiHtml,
createHTTPServer,
prettyDiagnostic,
PromiseQueue,
} from '@parcel/utils';

export type HMRAsset = {|
id: string,
url: string,
type: string,
output: string,
envHash: string,
outputFormat: string,
depsByBundle: {[string]: {[string]: string, ...}, ...},
|};

Expand All @@ -40,22 +54,38 @@ export type HMRMessage =
|};

const FS_CONCURRENCY = 64;
const HMR_ENDPOINT = '/__parcel_hmr';

export default class HMRServer {
wss: WebSocket.Server;
unresolvedError: HMRMessage | null = null;
options: HMRServerOptions;
bundleGraph: BundleGraph<PackagedBundle> | null = null;
stopServer: ?() => Promise<void>;

constructor(options: HMRServerOptions) {
this.options = options;
}

start(): any {
this.wss = new WebSocket.Server(
this.options.devServer
? {server: this.options.devServer}
: {port: this.options.port},
);
async start() {
let server = this.options.devServer;
if (!server) {
let result = await createHTTPServer({
listener: (req, res) => {
setHeaders(res);
if (!this.handle(req, res)) {
res.statusCode = 404;
res.end();
}
},
});
server = result.server;
server.listen(this.options.port, this.options.host);
this.stopServer = result.stop;
} else {
this.options.addMiddleware?.((req, res) => this.handle(req, res));
}
this.wss = new WebSocket.Server({server});

this.wss.on('connection', ws => {
if (this.unresolvedError) {
Expand All @@ -65,13 +95,28 @@ export default class HMRServer {

// $FlowFixMe[incompatible-exact]
this.wss.on('error', err => this.handleSocketError(err));
}

let address = this.wss.address();
invariant(typeof address === 'object' && address != null);
return address.port;
handle(req: Request, res: Response): boolean {
let {pathname} = url.parse(req.originalUrl || req.url);
if (pathname != null && pathname.startsWith(HMR_ENDPOINT)) {
let id = pathname.slice(HMR_ENDPOINT.length + 1);
let bundleGraph = nullthrows(this.bundleGraph);
let asset = bundleGraph.getAssetById(id);
this.getHotAssetContents(asset).then(output => {
res.setHeader('Content-Type', mime.contentType(asset.type));
res.end(output);
});
return true;
}
return false;
}

stop() {
async stop() {
if (this.stopServer != null) {
await this.stopServer();
this.stopServer = null;
}
this.wss.close();
}

Expand Down Expand Up @@ -106,6 +151,7 @@ export default class HMRServer {

async emitUpdate(event: BuildSuccessEvent) {
this.unresolvedError = null;
this.bundleGraph = event.bundleGraph;

let changedAssets = new Set(event.changedAssets.values());
if (changedAssets.size === 0) return;
Expand Down Expand Up @@ -153,14 +199,13 @@ export default class HMRServer {

return {
id: event.bundleGraph.getAssetPublicId(asset),
url: getSourceURL(event.bundleGraph, asset),
url: this.getSourceURL(asset),
type: asset.type,
// No need to send the contents of non-JS assets to the client.
output:
asset.type === 'js'
? await getHotAssetContents(event.bundleGraph, asset)
: '',
asset.type === 'js' ? await this.getHotAssetContents(asset) : '',
envHash: asset.env.id,
outputFormat: asset.env.outputFormat,
depsByBundle,
};
});
Expand All @@ -173,6 +218,41 @@ export default class HMRServer {
});
}

async getHotAssetContents(asset: Asset): Promise<string> {
let output = await asset.getCode();
let bundleGraph = nullthrows(this.bundleGraph);
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: SOURCES_ENDPOINT + '/',
// $FlowFixMe
fs: asset.fs,
});

invariant(typeof sourcemapStringified === 'string');
output += `\n//# sourceMappingURL=${sourcemapStringified}`;
output += `\n//# sourceURL=${encodeURI(this.getSourceURL(asset))}\n`;
}

return output;
}

getSourceURL(asset: Asset): string {
let origin = '';
if (!this.options.devServer) {
origin = `http://${this.options.host || 'localhost'}:${
this.options.port
}`;
}
return origin + HMR_ENDPOINT + '/' + asset.id;
}

handleSocketError(err: ServerError) {
if (err.code === 'ECONNRESET') {
// This gets triggered on page refresh, ignore this
Expand Down Expand Up @@ -201,34 +281,3 @@ function getSpecifier(dep: Dependency): string {

return dep.specifier;
}

export async function getHotAssetContents(
bundleGraph: BundleGraph<PackagedBundle>,
asset: Asset,
): Promise<string> {
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;
}
30 changes: 5 additions & 25 deletions packages/reporters/dev-server/src/Server.js
Expand Up @@ -29,13 +29,10 @@ import connect from 'connect';
import serveHandler from 'serve-handler';
import {createProxyMiddleware} from 'http-proxy-middleware';
import {URL, URLSearchParams} from 'url';
import {getHotAssetContents} from './HMRServer';
import nullthrows from 'nullthrows';
import mime from 'mime-types';
import launchEditor from 'launch-editor';
import fresh from 'fresh';

function setHeaders(res: Response) {
export function setHeaders(res: Response) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader(
'Access-Control-Allow-Methods',
Expand All @@ -48,8 +45,7 @@ function setHeaders(res: Response) {
res.setHeader('Cache-Control', 'max-age=0, must-revalidate');
}

const SOURCES_ENDPOINT = '/__parcel_source_root';
export const HMR_ENDPOINT = '/__parcel_hmr/';
export const SOURCES_ENDPOINT = '/__parcel_source_root';
const EDITOR_ENDPOINT = '/__parcel_launch_editor';
const TEMPLATE_404 = fs.readFileSync(
path.join(__dirname, 'templates/404.html'),
Expand All @@ -65,6 +61,7 @@ type NextFunction = (req: Request, res: Response, next?: (any) => any) => any;
export default class Server {
pending: boolean;
pendingRequests: Array<[Request, Response]>;
middleware: Array<(req: Request, res: Response) => boolean>;
options: DevServerOptions;
rootPath: string;
bundleGraph: BundleGraph<PackagedBundle> | null;
Expand All @@ -87,6 +84,7 @@ export default class Server {
}
this.pending = true;
this.pendingRequests = [];
this.middleware = [];
this.bundleGraph = null;
this.requestBundle = null;
this.errors = null;
Expand Down Expand Up @@ -135,8 +133,8 @@ export default class Server {
}

respond(req: Request, res: Response): mixed {
if (this.middleware.some(handler => handler(req, res))) return;
let {pathname, search} = url.parse(req.originalUrl || req.url);

if (pathname == null) {
pathname = '/';
}
Expand All @@ -151,13 +149,9 @@ export default class Server {
}
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);
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.
Expand Down Expand Up @@ -266,16 +260,6 @@ 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,
Expand Down Expand Up @@ -369,19 +353,15 @@ export default class Server {

sendError(res: Response, statusCode: number) {
res.statusCode = statusCode;
setHeaders(res);
res.end();
}

send404(req: Request, res: Response) {
res.statusCode = 404;
setHeaders(res);
res.end(TEMPLATE_404);
}

send500(req: Request, res: Response): void | Response {
setHeaders(res);

res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.writeHead(500);

Expand Down

0 comments on commit 52296ad

Please sign in to comment.