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

How can a bundler efficiently work across different sims or entry points? #1559

Open
samreid opened this issue Feb 15, 2025 · 2 comments
Open
Assignees

Comments

@samreid
Copy link
Member

samreid commented Feb 15, 2025

In phetsims/membrane-channels#6, we exercised esbuild to iterate quickly on membrane-channels development. One recurring question has been: how do we use a bundler across many entry points? For instance, if I need to run a test in sim1, then run a test in sim2, how can bundle.js be up-to-date for both without too many watch processes?

@samreid samreid self-assigned this Feb 15, 2025
@samreid
Copy link
Member Author

samreid commented Feb 15, 2025

Here is one working strategy, it runs a server that detects requests for bundle.js and runs esbuild before serving it. This is working correctly in membrane-channels but has not been generalized for use across other entry points. I'm also not sure how we could combine this with esbuild's live reload at all. Also, it's unclear how we could run this service as the server on phettest (I don't know how that server is configured).

// server.js
const http = require( 'http' );
const fs = require( 'fs' );
const path = require( 'path' );
const { execFile } = require( 'child_process' );
const url = require( 'url' );

// Set the port you want the server to listen on.
const PORT = 8080;

// The esbuild command in our example uses --servedir=..
// so we assume our static files live one directory above this script.
const STATIC_ROOT = path.join( __dirname, '..' );

// A very basic mapping from file extension to Content-Type.
function getContentType( filePath ) {
  const ext = path.extname( filePath ).toLowerCase();
  switch( ext ) {
    case '.html':
      return 'text/html';
    case '.js':
      return 'application/javascript';
    case '.css':
      return 'text/css';
    case '.json':
      return 'application/json';
    case '.map':
      return 'application/json';
    default:
      return 'text/plain';
  }
}

const server = http.createServer( ( req, res ) => {
  const parsedUrl = url.parse( req.url );
  let pathname = parsedUrl.pathname;

  // If the request is for the root path, serve index.html.
  if ( pathname === '/' ) {
    pathname = '/index.html';
  }

  // If the requested file is "bundle.js", trigger the esbuild process.
  if ( pathname.endsWith( 'bundle.js' ) ) {
    console.log( `Received request for ${pathname}. Triggering esbuild...` );

    // Define the esbuild arguments based on your exemplar.
    // Note that we remove the --watch, --servedir, and --serve flags
    // because we're doing an on-demand build.
    const esbuildArgs = [
      './membrane-channels/js/membrane-channels-main.ts', // entry file
      '--bundle',
      '--format=iife',
      '--global-name=MembraneChannelsBundle',
      '--outfile=dist/bundle.js',
      '--sourcemap'
    ];

    // We use 'npx' to run esbuild.
    execFile( 'npx', [ 'esbuild', ...esbuildArgs ], { cwd: STATIC_ROOT }, ( err, stdout, stderr ) => {
      if ( err ) {
        console.error( 'Esbuild error:', stderr );
        res.statusCode = 500;
        res.setHeader( 'Content-Type', 'text/plain' );
        res.end( 'Build failed:\n' + stderr );
        return;
      }

      console.log( 'Esbuild completed successfully. Serving bundle.js.' );

      // The output file is generated at "dist/bundle.js" relative to STATIC_ROOT.
      const bundlePath = path.join( STATIC_ROOT, 'dist', 'bundle.js' );
      fs.readFile( bundlePath, ( err, data ) => {
        if ( err ) {
          console.error( 'Error reading bundle.js:', err );
          res.statusCode = 500;
          res.setHeader( 'Content-Type', 'text/plain' );
          res.end( 'Error reading bundle file.' );
          return;
        }
        // Serve the file with no caching.
        res.statusCode = 200;
        res.setHeader( 'Content-Type', 'application/javascript' );
        res.setHeader( 'Cache-Control', 'no-store' );
        res.end( data );
      } );
    } );
  }
  else {
    // For all other requests, serve static files from STATIC_ROOT.
    const filePath = path.join( STATIC_ROOT, pathname );
    fs.readFile( filePath, ( err, data ) => {
      if ( err ) {
        console.error( `File not found: ${filePath}` );
        res.statusCode = 404;
        res.setHeader( 'Content-Type', 'text/plain' );
        res.end( 'File not found.' );
      }
      else {
        res.statusCode = 200;
        res.setHeader( 'Content-Type', getContentType( filePath ) );
        res.end( data );
      }
    } );
  }
} );

server.listen( PORT, () => {
  console.log( `Server running at http://localhost:${PORT}/` );
} );

@samreid
Copy link
Member Author

samreid commented Feb 15, 2025

This version works for any sim main, and is very snappy:

// server.js
const http = require( 'http' );
const fs = require( 'fs' );
const path = require( 'path' );
const { execFile } = require( 'child_process' );
const url = require( 'url' );

// Set the port you want the server to listen on.
const PORT = 8080;

// The esbuild command in our example uses --servedir=..
// so we assume our static files live one directory above this script.
const STATIC_ROOT = path.join( __dirname, '..' );

// A very basic mapping from file extension to Content-Type.
function getContentType( filePath ) {
  const ext = path.extname( filePath ).toLowerCase();
  switch( ext ) {
    case '.html':
      return 'text/html';
    case '.js':
      return 'application/javascript';
    case '.css':
      return 'text/css';
    case '.json':
      return 'application/json';
    case '.map':
      return 'application/json';
    default:
      return 'text/plain';
  }
}

const server = http.createServer( ( req, res ) => {
  const parsedUrl = url.parse( req.url );
  let pathname = parsedUrl.pathname;

  // If the request is for the root path, serve index.html.
  if ( pathname === '/' ) {
    pathname = '/index.html';
  }

  // If the requested file is "bundle.js", trigger the esbuild process.
  if ( pathname.endsWith( 'bundle.js' ) ) {
    console.log( `Received request for ${pathname}. Triggering esbuild...` );

    const sim = pathname.split( '/' )[ 1 ];
    console.log( 'sim = ', sim );

    // Define the esbuild arguments based on your exemplar.
    // Note that we remove the --watch, --servedir, and --serve flags
    // because we're doing an on-demand build.
    const esbuildArgs = [
      `./${sim}/js/${sim}-main.ts`, // entry file
      '--bundle',
      '--format=iife',
      '--global-name=MembraneChannelsBundle',
      '--outfile=dist/bundle.js',
      '--sourcemap'
    ];

    //mkdir dist
    // fs.mkdir( path.join( STATIC_ROOT, 'dist' ), { recursive: true }, ( err ) => {
    //   if ( err ) {
    //     console.error( 'Error creating dist directory:', err );
    //     res.statusCode = 500;
    //     res.setHeader( 'Content-Type', 'text/plain' );
    //     res.end( 'Error creating dist directory.' );
    //     return;
    //   }
    // } );

    // We use 'npx' to run esbuild.
    execFile( 'npx', [ 'esbuild', ...esbuildArgs ], { cwd: STATIC_ROOT }, ( err, stdout, stderr ) => {
      if ( err ) {
        console.error( 'Esbuild error:', stderr );
        res.statusCode = 500;
        res.setHeader( 'Content-Type', 'text/plain' );
        res.end( 'Build failed:\n' + stderr );
        return;
      }

      console.log( 'Esbuild completed successfully. Serving bundle.js.' );

      // The output file is generated at "dist/bundle.js" relative to STATIC_ROOT.
      const bundlePath = path.join( STATIC_ROOT, 'dist', 'bundle.js' );
      fs.readFile( bundlePath, ( err, data ) => {
        if ( err ) {
          console.error( 'Error reading bundle.js:', err );
          res.statusCode = 500;
          res.setHeader( 'Content-Type', 'text/plain' );
          res.end( 'Error reading bundle file.' );
          return;
        }
        // Serve the file with no caching.
        res.statusCode = 200;
        res.setHeader( 'Content-Type', 'application/javascript' );
        res.setHeader( 'Cache-Control', 'no-store' );
        res.end( data );
      } );
    } );
  }
  else {
    // For all other requests, serve static files from STATIC_ROOT.
    const filePath = path.join( STATIC_ROOT, pathname );
    fs.readFile( filePath, ( err, data ) => {
      if ( err ) {
        console.error( `File not found: ${filePath}` );
        res.statusCode = 404;
        res.setHeader( 'Content-Type', 'text/plain' );
        res.end( 'File not found.' );
      }
      else {
        res.statusCode = 200;
        res.setHeader( 'Content-Type', getContentType( filePath ) );
        res.end( data );
      }
    } );
  }
} );

server.listen( PORT, () => {
  console.log( `Server running at http://localhost:${PORT}/` );
} );

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant