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

esbuild config file #952

Closed
ebeloded opened this issue Mar 11, 2021 · 7 comments
Closed

esbuild config file #952

ebeloded opened this issue Mar 11, 2021 · 7 comments

Comments

@ebeloded
Copy link

It would be very useful to be able to provide esbuild with a config, instead of having to provide individual arguments, or use the JavaScript API

Here is a sample esbuild.config.ts

export default {
  entryPoints: ['./src/index.ts'],
  bundle: true,
  platform: 'node',
  outfile: 'build/main.js',
  sourcemap: true,
  target: 'node12',
  external: Object.keys(require('../package.json').dependencies),
}

CLI commands

> esbuild --config='esbuild.config.ts'

# any additional arguments would override config values
> esbuild --config='esbuild.config.ts' --watch

Furthermore, the name esbuild.config.(json|js|ts) could be considered as a standard name, and picked up automatically. then the CLI call would be as simple as

> esbuild
@evanw
Copy link
Owner

evanw commented Mar 11, 2021

The API intentionally uses API calls instead of config files. This is documented in the getting started guide: https://esbuild.github.io/getting-started/#build-scripts.

So instead of doing what you proposed:

$ cat > esbuild.config.js
export default {
  entryPoints: ['./src/index.ts'],
  bundle: true,
  platform: 'node',
  outfile: 'build/main.js',
  sourcemap: true,
  target: 'node12',
  external: Object.keys(require('../package.json').dependencies),
}

$ esbuild

You should do this instead:

$ cat > esbuild.config.js
require('esbuild').build({
  entryPoints: ['./src/index.ts'],
  bundle: true,
  platform: 'node',
  outfile: 'build/main.js',
  sourcemap: true,
  target: 'node12',
  external: Object.keys(require('../package.json').dependencies),
})

$ node esbuild.config.js

API calls are equivalent in length but more powerful than config files. Some benefits of API calls over config files:

  • API calls automatically give autocompletion and type hinting in TypeScript-enabled IDEs such as VSCode
  • API calls naturally extend to more complex use cases while config files don't (e.g. invoking esbuild multiple times, invoking esbuild inside itself, embedding esbuild into other tools)
  • The API is simpler because you don't need to learn about a completely different way of invoking esbuild once a config file is no longer sufficient

If you must, you can implement "config file support" yourself by making your own shim command that just calls require('esbuild').build(require('./esbuild.config.js')) and invoking that instead. But that's not how I intend for people to use the API and that's not something I plan to recommend in the documentation or to design the API for.

@ebeloded
Copy link
Author

ebeloded commented Mar 11, 2021

Thank you for prompt response!

I see the natural need in having the API for more complex tasks, yet access to the API is not necessary in most cases. Here are a couple more points:

  1. Type support and autocompletion are not lost in the case of config file:
import { BuildOptions } from 'esbuild'

export default {
  platform: 'node'
} as BuildOptions
  1. The setup gets uglier in a common scenario of using the same configuration, but in watch vs build mode. Here is a snippet that I use to run esbuild and pick up --watch flag:
function build(args) {
  require('esbuild')
    .build({
      ...buildOptions,
      watch: args.includes('--watch'),
    })
    .catch(() => process.exit(1))
}
build(process.argv.slice(2))
> node build.js 
> node build.js --watch

This piece of code is relatively simple, but I use my own flag, mimicking esbuild argument. Plus, as I use esbuild for a number of projects, this code is duplicated in each one of them and is not relevant to the build configuration.

Because the API is the only option, every developer using esbuild is forced to write a version of this script in their code. I'd prefer a simpler way, but I may not be in the majority.

@zaydek
Copy link

zaydek commented Mar 13, 2021

@ebeloded I’ve done something like this previously (ignore the comments):

// TODO: Ensure that serverProps and serverPaths cannot be aliased because of
// minification.
export const transpileOnlyConfiguration = (src: string, dst: string): esbuild.BuildOptions => ({
  bundle: true,
  define: {
    "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
  },
  entryPoints: [src],
  external: ["react", "react-dom"], // TODO: Use external strategy as defined in esnode?
  format: "cjs", // For require
  inject: ["packages/retro/react-shim.js"],
  loader: {
    ".js": "jsx",
  },
  minify: false,
  outfile: dst,
  // plugins: [...configs.retro.plugins],
})

// TODO: Ensure that serverProps and serverPaths cannot be aliased because of
// minification.
export const bundleConfiguration = (src: string, dst: string): esbuild.BuildOptions => ({
  bundle: true,
  define: {
    "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
  },
  entryPoints: [src],
  external: [],
  format: "iife",
  inject: ["packages/retro/react-shim.js"],
  loader: {
    ".js": "jsx",
  },
  minify: true,
  outfile: dst,
  // plugins: [...configs.retro.plugins],
})

The point being that these configuration files could theoretically be published to NPM or even linked (using npm link or whatever the command is). Then you could just pull in your configuration as you need it, which could be an object or a function like I’ve done.

Evan makes a good point though, which is a) thinking in JS rather than explicitly as data structures provides more flexibility and b) because there are more ways to invoke esbuild than only once, (incremental, watch, etc.), sometimes you really need to have more granular control over esbuild’s lifecycle than just one-shot invoking it.

Anyway, do you see any problem with creating configurations in TS and transpiling or in JS and publishing to NPM / linking so you can reuse your configurations, then using the spread operator like you’ve done above so you can decorate your configuration as you need? I also do that when using my example configurations listed above:

buildResult = await esbuild.build({
  ...esbuildHelpers.bundleConfiguration(src, dst),
  incremental: true,
  minify: false,
  watch: {
    async onRebuild(error) {
      if (error !== null) {
        if (!("errors" in error) || !("warnings" in error)) throw error
        await buildFailure.send(error)
      }
    },
  },
})

I hope seeing these contextual examples help you.

I think the source of the problem is that esbuild is so powerful that it requires a little more handling than other tooling. You could take this as a good or bad thing, but the point is esbuild’s incremental / watch modes make it a little more subtle than most one-shot tooling, which is probably more familiar.

@gopal-virtual
Copy link

@ebeloded the starter package takes a similar approach to make a common config and extending it for development and production environment. You might want to have a look.

@evanw
Copy link
Owner

evanw commented May 14, 2021

I'm closing this since the API intentionally uses API calls instead of a config file, as described above. If you still want a config file the workaround above should work (create a wrapper command that reads from the config file).

@evanw evanw closed this as completed May 14, 2021
olsonpm added a commit to olsonpm/feed-extractor that referenced this issue Dec 1, 2023
note: esbuild doesn't have a config like most tooling.  They suggest
instead to use their api.  See
evanw/esbuild#952 (comment)
@frenzymind
Copy link

@ebeloded Hello. May you please explain how do you use .ts file inside .js ? I reference for this answer: your post above
Your config file is ts couz you use types there, for the same time build file is js node build.js

My code is:
config.dev.ts

import type { BuildOptions } from "esbuild";

export const devOptions: BuildOptions = {
  logLevel: "info",
  bundle: true,
  platform: "node",
  format: "esm",
  sourcemap: true,
  entryPoints: ["src/index.ts"],
  outfile: "dist/index.js",
};

build file:

import { build } from "esbuild";
import { devOptions } from "./build/config.dev";

await build({
  ...devOptions,
}).catch(() => process.exit(1));

but when I try to run I get error:

root@ndev:/home/node-ts-template# node esbuild.js 
node:internal/errors:496
    ErrorCaptureStackTrace(err);
    ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/node-ts-template/build/config.dev' imported from /home/node-ts-template/esbuild.js
    at new NodeError (node:internal/errors:405:5)
    at finalizeResolution (node:internal/modules/esm/resolve:327:11)
    at moduleResolve (node:internal/modules/esm/resolve:980:10)
    at defaultResolve (node:internal/modules/esm/resolve:1193:11)
    at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:403:12)
    at ModuleLoader.resolve (node:internal/modules/esm/loader:372:25)
    at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:249:38)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:76:39)
    at link (node:internal/modules/esm/module_job:75:36) {
  url: 'file:///home/node-ts-template/build/config.dev',
  code: 'ERR_MODULE_NOT_FOUND'
}

Node.js v18.19.0

I have "type": "module", in package.json , and my tsconfig:

{
  "compilerOptions": {
    /* Base Options: */
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "es2022",
    "verbatimModuleSyntax": true,
    "allowJs": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",

    /* Strictness */
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedParameters": true,

    /* If NOT transpiling with TypeScript(tsc): */
    "moduleResolution": "Bundler",
    "module": "ESNext",
    "noEmit": true,

    /* If your code doesn't run in the DOM: */
    "lib": ["es2022"]
  }
}

@tpluscode
Copy link

tpluscode commented Sep 27, 2024

Found these today which can be useful alternatives

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

6 participants