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

Support running node bundles in dev server #10055

Merged
merged 3 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/core/core/src/BundleGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -2107,4 +2107,33 @@ export default class BundleGraph {
this._targetEntryRoots.set(target.distDir, root);
return root;
}

getEntryBundles(): Array<Bundle> {
let entryBundleGroupIds = this._graph.getNodeIdsConnectedFrom(
nullthrows(this._graph.rootNodeId),
bundleGraphEdgeTypes.bundle,
);

let entries = [];
for (let bundleGroupId of entryBundleGroupIds) {
let bundleGroupNode = this._graph.getNode(bundleGroupId);
invariant(bundleGroupNode?.type === 'bundle_group');

let entryBundle = this.getBundlesInBundleGroup(
bundleGroupNode.value,
).find(b => {
let mainEntryId = b.entryAssetIds[b.entryAssetIds.length - 1];
return (
mainEntryId != null &&
bundleGroupNode.value.entryAssetId === mainEntryId
);
});

if (entryBundle) {
entries.push(entryBundle);
}
}

return entries;
}
}
6 changes: 6 additions & 0 deletions packages/core/core/src/public/BundleGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,4 +332,10 @@ export default class BundleGraph<TBundle: IBundle>
targetToInternalTarget(target),
);
}

getEntryBundles(): Array<TBundle> {
return this.#graph
.getEntryBundles()
.map(b => this.#createBundle(b, this.#graph, this.#options));
}
}
4 changes: 2 additions & 2 deletions packages/core/logger/src/Logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ export class PluginLogger implements IPluginLogger {
): Diagnostic | Array<Diagnostic> {
return Array.isArray(diagnostic)
? diagnostic.map(d => {
return {...d, origin: this.origin};
return {...d, origin: d.origin ?? this.origin};
})
: {...diagnostic, origin: this.origin};
: {...diagnostic, origin: diagnostic.origin ?? this.origin};
}

verbose(
Expand Down
2 changes: 2 additions & 0 deletions packages/core/types-internal/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1622,6 +1622,8 @@ export interface BundleGraph<TBundle: Bundle> {
getUsedSymbols(Asset | Dependency): ?$ReadOnlySet<Symbol>;
/** Returns the common root directory for the entry assets of a target. */
getEntryRoot(target: Target): FilePath;
/** Returns a list of entry bundles. */
getEntryBundles(): Array<TBundle>;
}

/**
Expand Down
8 changes: 5 additions & 3 deletions packages/examples/react-server-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
}
}
},
"scripts": {
"dev": "parcel",
"build": "parcel build",
"start": "node dist/server.js"
},
"dependencies": {
"express": "^4.18.2",
"react": "^19",
"react-dom": "^19",
"react-server-dom-parcel": "^0.0.1",
"rsc-html-stream": "^0.0.4"
},
"@parcel/resolver-default": {
"packageExports": true
}
}
27 changes: 15 additions & 12 deletions packages/reporters/cli/src/CLIReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,6 @@ export async function _report(
// Clear any previous output
resetWindow();

if (options.serveOptions) {
persistMessage(
chalk.blue.bold(
`Server running at ${
options.serveOptions.https ? 'https' : 'http'
}://${options.serveOptions.host ?? 'localhost'}:${
options.serveOptions.port
}`,
),
);
}

break;
}
case 'buildProgress': {
Expand Down Expand Up @@ -121,6 +109,21 @@ export async function _report(

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

if (
options.serveOptions &&
event.bundleGraph.getEntryBundles().some(b => b.env.isBrowser())
) {
persistMessage(
chalk.blue.bold(
`Server running at ${
options.serveOptions.https ? 'https' : 'http'
}://${options.serveOptions.host ?? 'localhost'}:${
options.serveOptions.port
}`,
),
);
}

persistSpinner(
'buildProgress',
'success',
Expand Down
10 changes: 8 additions & 2 deletions packages/reporters/dev-server/src/HMRServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,9 @@ export default class HMRServer {
if (sourcemap) {
let sourcemapStringified = await sourcemap.stringify({
format: 'inline',
sourceRoot: SOURCES_ENDPOINT + '/',
sourceRoot:
(asset.env.isNode() ? this.options.projectRoot : SOURCES_ENDPOINT) +
'/',
// $FlowFixMe
fs: asset.fs,
});
Expand All @@ -266,7 +268,11 @@ export default class HMRServer {

getSourceURL(asset: Asset): string {
let origin = '';
if (!this.options.devServer) {
if (
!this.options.devServer ||
// $FlowFixMe
this.bundleGraph?.getEntryBundles().some(b => b.env.isServer())
) {
origin = `http://${this.options.host || 'localhost'}:${
this.options.port
}`;
Expand Down
111 changes: 111 additions & 0 deletions packages/reporters/dev-server/src/NodeRunner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// @flow
import type {PluginLogger, BundleGraph, PackagedBundle} from '@parcel/types';

import {md, errorToDiagnostic} from '@parcel/diagnostic';
import nullthrows from 'nullthrows';
import {Worker} from 'worker_threads';
import path from 'path';

export type NodeRunnerOptions = {|
hmr: boolean,
logger: PluginLogger,
|};

export class NodeRunner {
worker: Worker | null = null;
bundleGraph: BundleGraph<PackagedBundle> | null = null;
pending: boolean = true;
logger: PluginLogger;
hmr: boolean;

constructor(options: NodeRunnerOptions) {
this.logger = options.logger;
this.hmr = options.hmr;
}

buildStart() {
this.pending = true;
}

buildSuccess(bundleGraph: BundleGraph<PackagedBundle>) {
this.bundleGraph = bundleGraph;
this.pending = false;
if (this.worker == null) {
this.startWorker();
} else if (!this.hmr) {
this.restartWorker();
}
}

startWorker() {
let entry = nullthrows(this.bundleGraph)
.getEntryBundles()
.find(b => b.env.isNode() && b.type === 'js');
if (entry) {
let relativePath = path.relative(process.cwd(), entry.filePath);
this.logger.log({message: md`Starting __${relativePath}__...`});
let worker = new Worker(entry.filePath, {
execArgv: ['--enable-source-maps'],
workerData: {
// Used by the hmr-runtime to detect when to send restart messages.
__parcel: true,
},
stdout: true,
stderr: true,
});

worker.on('message', msg => {
if (msg === 'restart') {
this.restartWorker();
}
});

worker.on('error', (err: Error) => {
this.logger.error(errorToDiagnostic(err));
});

worker.stderr.setEncoding('utf8');
worker.stderr.on('data', data => {
for (let line of data.split('\n')) {
this.logger.error({
origin: relativePath,
message: line,
skipFormatting: true,
});
}
});

worker.stdout.setEncoding('utf8');
worker.stdout.on('data', data => {
for (let line of data.split('\n')) {
this.logger.log({
origin: relativePath,
message: line,
skipFormatting: true,
});
}
});

worker.on('exit', () => {
this.worker = null;
});

this.worker = worker;
}
}

async stop(): Promise<void> {
await this.worker?.terminate();
this.worker = null;
}

async restartWorker(): Promise<void> {
await this.stop();

// HMR updates are sent before packaging is complete.
// If the build is still pending, wait until it completes to restart.
if (!this.pending) {
this.startWorker();
}
}
}
15 changes: 14 additions & 1 deletion packages/reporters/dev-server/src/ServerReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
import {Reporter} from '@parcel/plugin';
import HMRServer from './HMRServer';
import Server from './Server';
import {NodeRunner} from './NodeRunner';

let servers: Map<number, Server> = new Map();
let hmrServers: Map<number, HMRServer> = new Map();
let nodeRunners: Map<string, NodeRunner> = new Map();
export default (new Reporter({
async report({event, options, logger}) {
let {serveOptions, hmrOptions} = options;
let server = serveOptions ? servers.get(serveOptions.port) : undefined;
let hmrPort =
(hmrOptions && hmrOptions.port) || (serveOptions && serveOptions.port);
let hmrServer = hmrPort ? hmrServers.get(hmrPort) : undefined;
let nodeRunner = nodeRunners.get(options.instanceId);
switch (event.type) {
case 'watchStart': {
if (serveOptions) {
Expand Down Expand Up @@ -55,6 +58,7 @@ export default (new Reporter({
cacheDir: options.cacheDir,
inputFS: options.inputFS,
outputFS: options.outputFS,
projectRoot: options.projectRoot,
};
hmrServer = new HMRServer(hmrServerOptions);
hmrServers.set(serveOptions.port, hmrServer);
Expand All @@ -73,6 +77,7 @@ export default (new Reporter({
cacheDir: options.cacheDir,
inputFS: options.inputFS,
outputFS: options.outputFS,
projectRoot: options.projectRoot,
};
hmrServer = new HMRServer(hmrServerOptions);
hmrServers.set(port, hmrServer);
Expand Down Expand Up @@ -101,6 +106,7 @@ export default (new Reporter({
if (server) {
server.buildStart();
}
nodeRunner?.buildStart();
break;
case 'buildProgress':
if (
Expand All @@ -113,7 +119,7 @@ export default (new Reporter({
await hmrServer.emitUpdate(event);
}
break;
case 'buildSuccess':
case 'buildSuccess': {
if (serveOptions) {
if (!server) {
return logger.warn({
Expand All @@ -127,7 +133,14 @@ export default (new Reporter({
if (hmrServer && options.serveOptions === false) {
await hmrServer.emitUpdate(event);
}

if (!nodeRunner && options.serveOptions) {
nodeRunner = new NodeRunner({logger, hmr: !!options.hmrOptions});
nodeRunners.set(options.instanceId, nodeRunner);
}
nodeRunner?.buildSuccess(event.bundleGraph);
break;
}
case 'buildFailure':
// On buildFailure watchStart sometimes has not been called yet
// do not throw an additional warning here
Expand Down
1 change: 1 addition & 0 deletions packages/reporters/dev-server/src/types.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,5 @@ export type HMRServerOptions = {|
cacheDir: FilePath,
inputFS: FileSystem,
outputFS: FileSystem,
projectRoot: FilePath,
|};
10 changes: 8 additions & 2 deletions packages/runtimes/hmr/src/HMRRuntime.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const HMR_RUNTIME = fs.readFileSync(
);

export default (new Runtime({
apply({bundle, options}) {
apply({bundle, bundleGraph, options}) {
if (
bundle.type !== 'js' ||
!options.hmrOptions ||
Expand All @@ -31,6 +31,10 @@ export default (new Runtime({
}

const {host, port} = options.hmrOptions;
let hasServerBundles = bundleGraph
.getEntryBundles()
.some(b => b.env.isServer());

return {
filePath: FILENAME,
code:
Expand All @@ -41,7 +45,9 @@ export default (new Runtime({
port != null &&
// Default to the HTTP port in the browser, only override
// in watch mode or if hmr port != serve port
(!options.serveOptions || options.serveOptions.port !== port)
(!options.serveOptions ||
options.serveOptions.port !== port ||
hasServerBundles)
? port
: null,
)};` +
Expand Down
15 changes: 12 additions & 3 deletions packages/runtimes/hmr/src/loaders/hmr-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,18 @@ function fullReload() {
) {
extCtx.runtime.reload();
} else {
console.error(
'[parcel] ⚠️ An HMR update was not accepted. Please restart the process.',
);
try {
let {workerData, parentPort} = (module.bundle.root(
'node:worker_threads',
) /*: any*/);
if (workerData?.__parcel) {
parentPort.postMessage('restart');
}
} catch (err) {
console.error(
'[parcel] ⚠️ An HMR update was not accepted. Please restart the process.',
);
}
}
}

Expand Down
Loading
Loading