Skip to content

Commit 6e49cda

Browse files
committed
Support running node bundles in dev server
1 parent ee5f98a commit 6e49cda

File tree

12 files changed

+213
-22
lines changed

12 files changed

+213
-22
lines changed

packages/core/core/src/BundleGraph.js

+29
Original file line numberDiff line numberDiff line change
@@ -2107,4 +2107,33 @@ export default class BundleGraph {
21072107
this._targetEntryRoots.set(target.distDir, root);
21082108
return root;
21092109
}
2110+
2111+
getEntryBundles(): Array<Bundle> {
2112+
let entryBundleGroupIds = this._graph.getNodeIdsConnectedFrom(
2113+
nullthrows(this._graph.rootNodeId),
2114+
bundleGraphEdgeTypes.bundle,
2115+
);
2116+
2117+
let entries = [];
2118+
for (let bundleGroupId of entryBundleGroupIds) {
2119+
let bundleGroupNode = this._graph.getNode(bundleGroupId);
2120+
invariant(bundleGroupNode?.type === 'bundle_group');
2121+
2122+
let entryBundle = this.getBundlesInBundleGroup(
2123+
bundleGroupNode.value,
2124+
).find(b => {
2125+
let mainEntryId = b.entryAssetIds[b.entryAssetIds.length - 1];
2126+
return (
2127+
mainEntryId != null &&
2128+
bundleGroupNode.value.entryAssetId === mainEntryId
2129+
);
2130+
});
2131+
2132+
if (entryBundle) {
2133+
entries.push(entryBundle);
2134+
}
2135+
}
2136+
2137+
return entries;
2138+
}
21102139
}

packages/core/core/src/public/BundleGraph.js

+6
Original file line numberDiff line numberDiff line change
@@ -332,4 +332,10 @@ export default class BundleGraph<TBundle: IBundle>
332332
targetToInternalTarget(target),
333333
);
334334
}
335+
336+
getEntryBundles(): Array<TBundle> {
337+
return this.#graph
338+
.getEntryBundles()
339+
.map(b => this.#createBundle(b, this.#graph, this.#options));
340+
}
335341
}

packages/core/logger/src/Logger.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@ export class PluginLogger implements IPluginLogger {
102102
): Diagnostic | Array<Diagnostic> {
103103
return Array.isArray(diagnostic)
104104
? diagnostic.map(d => {
105-
return {...d, origin: this.origin};
105+
return {...d, origin: d.origin ?? this.origin};
106106
})
107-
: {...diagnostic, origin: this.origin};
107+
: {...diagnostic, origin: diagnostic.origin ?? this.origin};
108108
}
109109

110110
verbose(

packages/core/types-internal/src/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,8 @@ export interface BundleGraph<TBundle: Bundle> {
16221622
getUsedSymbols(Asset | Dependency): ?$ReadOnlySet<Symbol>;
16231623
/** Returns the common root directory for the entry assets of a target. */
16241624
getEntryRoot(target: Target): FilePath;
1625+
/** Returns a list of entry bundles. */
1626+
getEntryBundles(): Array<TBundle>;
16251627
}
16261628

16271629
/**

packages/examples/react-server-components/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
}
1313
}
1414
},
15+
"scripts": {
16+
"dev": "parcel",
17+
"build": "parcel build",
18+
"start": "node dist/server.js"
19+
},
1520
"dependencies": {
1621
"express": "^4.18.2",
1722
"react": "^19",

packages/reporters/cli/src/CLIReporter.js

+15-12
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,6 @@ export async function _report(
5757
// Clear any previous output
5858
resetWindow();
5959

60-
if (options.serveOptions) {
61-
persistMessage(
62-
chalk.blue.bold(
63-
`Server running at ${
64-
options.serveOptions.https ? 'https' : 'http'
65-
}://${options.serveOptions.host ?? 'localhost'}:${
66-
options.serveOptions.port
67-
}`,
68-
),
69-
);
70-
}
71-
7260
break;
7361
}
7462
case 'buildProgress': {
@@ -121,6 +109,21 @@ export async function _report(
121109

122110
phaseStartTimes['buildSuccess'] = Date.now();
123111

112+
if (
113+
options.serveOptions &&
114+
event.bundleGraph.getEntryBundles().some(b => b.env.isBrowser())
115+
) {
116+
persistMessage(
117+
chalk.blue.bold(
118+
`Server running at ${
119+
options.serveOptions.https ? 'https' : 'http'
120+
}://${options.serveOptions.host ?? 'localhost'}:${
121+
options.serveOptions.port
122+
}`,
123+
),
124+
);
125+
}
126+
124127
persistSpinner(
125128
'buildProgress',
126129
'success',

packages/reporters/dev-server/src/HMRServer.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,9 @@ export default class HMRServer {
251251
if (sourcemap) {
252252
let sourcemapStringified = await sourcemap.stringify({
253253
format: 'inline',
254-
sourceRoot: SOURCES_ENDPOINT + '/',
254+
sourceRoot:
255+
(asset.env.isNode() ? this.options.projectRoot : SOURCES_ENDPOINT) +
256+
'/',
255257
// $FlowFixMe
256258
fs: asset.fs,
257259
});
@@ -266,7 +268,11 @@ export default class HMRServer {
266268

267269
getSourceURL(asset: Asset): string {
268270
let origin = '';
269-
if (!this.options.devServer) {
271+
// $FlowFixMe
272+
if (
273+
!this.options.devServer ||
274+
this.bundleGraph?.getEntryBundles().some(b => b.env.isServer())
275+
) {
270276
origin = `http://${this.options.host || 'localhost'}:${
271277
this.options.port
272278
}`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// @flow
2+
import type {PluginLogger, BundleGraph, PackagedBundle} from '@parcel/types';
3+
4+
import {md, errorToDiagnostic} from '@parcel/diagnostic';
5+
import nullthrows from 'nullthrows';
6+
import {Worker} from 'worker_threads';
7+
import path from 'path';
8+
9+
export type NodeRunnerOptions = {|
10+
hmr: boolean,
11+
logger: PluginLogger,
12+
|};
13+
14+
export class NodeRunner {
15+
worker: Worker | null = null;
16+
bundleGraph: BundleGraph<PackagedBundle> | null = null;
17+
pending: boolean = true;
18+
logger: PluginLogger;
19+
hmr: boolean;
20+
21+
constructor(options: NodeRunnerOptions) {
22+
this.logger = options.logger;
23+
this.hmr = options.hmr;
24+
}
25+
26+
buildStart() {
27+
this.pending = true;
28+
}
29+
30+
buildSuccess(bundleGraph: BundleGraph<PackagedBundle>) {
31+
this.bundleGraph = bundleGraph;
32+
this.pending = false;
33+
if (this.worker == null) {
34+
this.startWorker();
35+
} else if (!this.hmr) {
36+
this.restartWorker();
37+
}
38+
}
39+
40+
startWorker() {
41+
let entry = nullthrows(this.bundleGraph)
42+
.getEntryBundles()
43+
.find(b => b.env.isNode() && b.type === 'js');
44+
if (entry) {
45+
let relativePath = path.relative(process.cwd(), entry.filePath);
46+
this.logger.log({message: md`Starting __${relativePath}__...`});
47+
let worker = new Worker(entry.filePath, {
48+
execArgv: ['--enable-source-maps'],
49+
workerData: {
50+
// Used by the hmr-runtime to detect when to send restart messages.
51+
__parcel: true,
52+
},
53+
stdout: true,
54+
stderr: true,
55+
});
56+
57+
worker.on('message', msg => {
58+
if (msg === 'restart') {
59+
this.restartWorker();
60+
}
61+
});
62+
63+
worker.on('error', (err: Error) => {
64+
this.logger.error(errorToDiagnostic(err));
65+
});
66+
67+
worker.stderr.setEncoding('utf8');
68+
worker.stderr.on('data', data => {
69+
for (let line of data.split('\n')) {
70+
this.logger.error({
71+
origin: relativePath,
72+
message: line,
73+
skipFormatting: true,
74+
});
75+
}
76+
});
77+
78+
worker.stdout.setEncoding('utf8');
79+
worker.stdout.on('data', data => {
80+
for (let line of data.split('\n')) {
81+
this.logger.log({
82+
origin: relativePath,
83+
message: line,
84+
skipFormatting: true,
85+
});
86+
}
87+
});
88+
89+
worker.on('exit', () => {
90+
this.worker = null;
91+
});
92+
93+
this.worker = worker;
94+
}
95+
}
96+
97+
async stop(): Promise<void> {
98+
await this.worker?.terminate();
99+
this.worker = null;
100+
}
101+
102+
async restartWorker(): Promise<void> {
103+
await this.stop();
104+
105+
// HMR updates are sent before packaging is complete.
106+
// If the build is still pending, wait until it completes to restart.
107+
if (!this.pending) {
108+
this.startWorker();
109+
}
110+
}
111+
}

packages/reporters/dev-server/src/ServerReporter.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
import {Reporter} from '@parcel/plugin';
44
import HMRServer from './HMRServer';
55
import Server from './Server';
6+
import {NodeRunner} from './NodeRunner';
67

78
let servers: Map<number, Server> = new Map();
89
let hmrServers: Map<number, HMRServer> = new Map();
10+
let nodeRunners: Map<string, NodeRunner> = new Map();
911
export default (new Reporter({
1012
async report({event, options, logger}) {
1113
let {serveOptions, hmrOptions} = options;
1214
let server = serveOptions ? servers.get(serveOptions.port) : undefined;
1315
let hmrPort =
1416
(hmrOptions && hmrOptions.port) || (serveOptions && serveOptions.port);
1517
let hmrServer = hmrPort ? hmrServers.get(hmrPort) : undefined;
18+
let nodeRunner = nodeRunners.get(options.instanceId);
1619
switch (event.type) {
1720
case 'watchStart': {
1821
if (serveOptions) {
@@ -55,6 +58,7 @@ export default (new Reporter({
5558
cacheDir: options.cacheDir,
5659
inputFS: options.inputFS,
5760
outputFS: options.outputFS,
61+
projectRoot: options.projectRoot,
5862
};
5963
hmrServer = new HMRServer(hmrServerOptions);
6064
hmrServers.set(serveOptions.port, hmrServer);
@@ -73,6 +77,7 @@ export default (new Reporter({
7377
cacheDir: options.cacheDir,
7478
inputFS: options.inputFS,
7579
outputFS: options.outputFS,
80+
projectRoot: options.projectRoot,
7681
};
7782
hmrServer = new HMRServer(hmrServerOptions);
7883
hmrServers.set(port, hmrServer);
@@ -101,6 +106,7 @@ export default (new Reporter({
101106
if (server) {
102107
server.buildStart();
103108
}
109+
nodeRunner?.buildStart();
104110
break;
105111
case 'buildProgress':
106112
if (
@@ -113,7 +119,7 @@ export default (new Reporter({
113119
await hmrServer.emitUpdate(event);
114120
}
115121
break;
116-
case 'buildSuccess':
122+
case 'buildSuccess': {
117123
if (serveOptions) {
118124
if (!server) {
119125
return logger.warn({
@@ -127,7 +133,14 @@ export default (new Reporter({
127133
if (hmrServer && options.serveOptions === false) {
128134
await hmrServer.emitUpdate(event);
129135
}
136+
137+
if (!nodeRunner && options.serveOptions) {
138+
nodeRunner = new NodeRunner({logger, hmr: !!options.hmrOptions});
139+
nodeRunners.set(options.instanceId, nodeRunner);
140+
}
141+
nodeRunner?.buildSuccess(event.bundleGraph);
130142
break;
143+
}
131144
case 'buildFailure':
132145
// On buildFailure watchStart sometimes has not been called yet
133146
// do not throw an additional warning here

packages/reporters/dev-server/src/types.js.flow

+1
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ export type HMRServerOptions = {|
5353
cacheDir: FilePath,
5454
inputFS: FileSystem,
5555
outputFS: FileSystem,
56+
projectRoot: FilePath,
5657
|};

packages/runtimes/hmr/src/HMRRuntime.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const HMR_RUNTIME = fs.readFileSync(
1919
);
2020

2121
export default (new Runtime({
22-
apply({bundle, options}) {
22+
apply({bundle, bundleGraph, options}) {
2323
if (
2424
bundle.type !== 'js' ||
2525
!options.hmrOptions ||
@@ -31,6 +31,10 @@ export default (new Runtime({
3131
}
3232

3333
const {host, port} = options.hmrOptions;
34+
let hasServerBundles = bundleGraph
35+
.getEntryBundles()
36+
.some(b => b.env.isServer());
37+
3438
return {
3539
filePath: FILENAME,
3640
code:
@@ -41,7 +45,9 @@ export default (new Runtime({
4145
port != null &&
4246
// Default to the HTTP port in the browser, only override
4347
// in watch mode or if hmr port != serve port
44-
(!options.serveOptions || options.serveOptions.port !== port)
48+
(!options.serveOptions ||
49+
options.serveOptions.port !== port ||
50+
hasServerBundles)
4551
? port
4652
: null,
4753
)};` +

packages/runtimes/hmr/src/loaders/hmr-runtime.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,18 @@ function fullReload() {
329329
) {
330330
extCtx.runtime.reload();
331331
} else {
332-
console.error(
333-
'[parcel] ⚠️ An HMR update was not accepted. Please restart the process.',
334-
);
332+
try {
333+
let {workerData, parentPort} = (module.bundle.root(
334+
'node:worker_threads',
335+
) /*: any*/);
336+
if (workerData?.__parcel) {
337+
parentPort.postMessage('restart');
338+
}
339+
} catch (err) {
340+
console.error(
341+
'[parcel] ⚠️ An HMR update was not accepted. Please restart the process.',
342+
);
343+
}
335344
}
336345
}
337346

0 commit comments

Comments
 (0)