Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add manifest V3 true HMR #8145

Merged
merged 14 commits into from Jun 13, 2022
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);
devongovett marked this conversation as resolved.
Show resolved Hide resolved
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