egoist / tsup

The simplest and fastest way to bundle your TypeScript libraries.
https://tsup.egoist.dev
MIT License
8.99k stars 217 forks source link

Support for 'umd' and 'amd' formats #924

Open bo-carey opened 1 year ago

bo-carey commented 1 year ago

Hello! Are there any plans or existing solutions to format as UMD or AMD?

Upvote & Fund

Fund with Polar

weyert commented 1 year ago

I have been wanting to do the same. I am currently trying out the esbuild plugin named esbuild-plugin-umd-wrapper with the following added configuration to tsup.config.cjs:

esbuildPlugins: [
    umdWrapper({
      libraryName: 'library-name',
    })
  ],
  // Adding 'umd' to trigger the umdWrapper plugin
  format: ['cjs', 'esm', 'iife', 'umd'],

At first sight it appears to work as expected

dhowe commented 10 months ago

I get an error when I specify 'umd' as a format @weyert do you mind linking to your full tsup.config.js ?

dhowe commented 10 months ago

Or, to others, is there another way to create a UMD package with tsup ?

bo-carey commented 10 months ago

is there another way to create a UMD package with tsup?

@dhowe My org has been generating our bundles as esm and converting them to amd/umd after. Maybe that could be a workaround for you 🤷‍♂️

dhowe commented 10 months ago

thanks @bo-carey

generating our bundles as esm and converting them to amd/umd after

with what tooling ?

bo-carey commented 10 months ago

with what tooling ?

@dhowe Thankfully our library consumers use ember so they have access to transforming esm -> amd via the ember-cli. Otherwise, there are some small libraries out there to transform it, such as dcodeIO/esm2umd.

weyert commented 10 months ago

The following worked in my case:

tsup.config.cjs

import { defineConfig } from 'tsup'
import umdWrapper from 'esbuild-plugin-umd-wrapper'
import { dependencies } from './package.json'
import { createUmdWrapper } from './build-plugins/umdWrapperPlugin.cjs'

const externalDependencies = Object.keys(dependencies)

const sdkClientVersion = '1.0.0'
const clientName = `ClientName`

const isDevelopmentMode = process.env._DEV === 'true'

/** @type {import('tsup').Options} */
const baseConfig = {
  entry: {
    appstore: 'src/index.ts',
  },
  outDir: 'dist',
  outExtension({ format, options }) {
    const ext = format === 'esm' ? 'mjs' : 'js'
    const outputExtension = options.minify ? `${format}.min.${ext}` : `${format}.${ext}`
    return {
      js: `.${outputExtension}`,
    }
  },
  platform: 'browser',
  format: ['cjs', 'esm'],
  noExternal: externalDependencies,
  target: ['chrome90', 'edge90', 'firefox90', 'opera98', 'safari15'],
  name: '@company/client',
  globalName: clientName,
  legacyOutput: false,
  bundle: true,
  esbuildPlugins: [],
  banner: { js: `/* Client SDK version ${sdkClientVersion} */\n` },
  define: {
    __VERSION__: `'${sdkClientVersion}'`,
  },
  minify: false,
  splitting: false,
  sourcemap: true,
  dts: false,
  clean: true,
  onSuccess: 'tsc --project tsconfig.build.json --emitDeclarationOnly --declaration',
  watch: isDevelopmentMode,
  metafile: isDevelopmentMode,
}

export default defineConfig([
  {
    ...baseConfig,
    esbuildPlugins: [],
    minify: false,
  },
  {
    ...baseConfig,
    esbuildPlugins: [],
    minify: true,
  },
  {
    ...baseConfig,
    format: ['umd'],
    minify: false,
    plugins: [createUmdWrapper({ libraryName: clientName, external: [] })],
  },
  {
    ...baseConfig,
    minify: true,
    format: ['umd'],
    plugins: [createUmdWrapper({ libraryName: clientName, external: [] })],
  },
  {
    ...baseConfig,
    entry: {
      marketplace: 'src/index.ts',
    },
    minify: false,
    target: 'es5',
    format: ['umd'],
    outputExtension: {
      js: `browser.js`,
    },
    outDir: 'dist',
    esbuildPlugins: [umdWrapper({ libraryName: clientName, external: 'inherit' })],
  },
])

umdWrapperPlugin.cjs

import * as path from 'node:path'
import * as fs from 'node:fs'

/*
  Plugin is based on the `esbuild-plugin-umd-wrapper``, found at:
  https://github.com/inqnuam/esbuild-plugin-umd-wrapper
*/

// eslint-disable-next-line no-unused-vars
const createWrapperWithLib = ({ depsKeys, depsValKey, amdLoader, lib, defineDeps, globalDeps, requireDeps }) => {
  return `(function (g, f) {
      if ("object" == typeof exports && "object" == typeof module) {
        module.exports = f(${requireDeps});
      } else if ("function" == typeof ${amdLoader} && ${amdLoader}.amd) {
        ${amdLoader}("${lib}", ${defineDeps}, f);
      } else if ("object" == typeof exports) {
        exports["${lib}"] = f(${requireDeps});
      } else {
        g["${lib}"] = f(${globalDeps});
      }
    }(this, (${depsKeys}) => {
  var exports = {};
  var module = { exports };\n\n`
}

export const alphabet = [
  '__da',
  '__db',
  '__dc',
  '__dd',
  '__de',
  '__df',
  '__dg',
  '__dh',
  '__di',
  '__dj',
  '__dk',
  '__dl',
  '__dm',
  '__dn',
  '__do',
  '__dp',
  '__dq',
  '__dr',
  '__ds',
  '__dt',
  '__du',
  '__dv',
  '__dw',
  '__dx',
  '__dy',
  '__dz',
]

function getUmdBanner(opts) {
  const external = opts.external ?? []
  const defineDeps = external?.length ? `['${external.join("', '")}']` : '[]'
  const globalDeps = external?.map(x => `g["${x}"]`).join(', ') ?? ''
  const requireDeps = external?.map(x => `require('${x}')`).join(', ') ?? ''
  let deps = []
  if (external) {
    deps = external.map((x, i) => {
      return {
        key: alphabet[i],
        val: x,
      }
    })
  }
  const depsKeys = deps.map(x => x.key).join(', ')
  const depsValKey = deps.map(x => `"${x.val}": ${x.key}`).join(', ')
  const options = {
    depsKeys,
    depsValKey,
    amdLoader: 'define',
    defineDeps,
    globalDeps,
    requireDeps,
    lib: opts.libraryName,
  }
  const result = createWrapperWithLib(options)
  return result
}

export const umdFooter = `if (typeof module.exports == "object" && typeof exports == "object") {
    var __cp = (to, from, except, desc) => {
      if ((from && typeof from === "object") || typeof from === "function") {
        for (let key of Object.getOwnPropertyNames(from)) {
          if (!Object.prototype.hasOwnProperty.call(to, key) && key !== except)
          Object.defineProperty(to, key, {
            get: () => from[key],
            enumerable: !(desc = Object.getOwnPropertyDescriptor(from, key)) || desc.enumerable,
          });
        }
      }
      return to;
    };
    module.exports = __cp(module.exports, exports);
  }
  return module.exports;
  }))\n\n\n`

export const umdWrapperSetup = build => {
  const { initialOptions } = build
  const external = initialOptions.external
  const content = getUmdBanner(external)
  if (initialOptions.footer) {
    if (initialOptions.footer.js) {
      initialOptions.footer.js += umdFooter
    } else {
      initialOptions.footer.js = umdFooter
    }
  } else {
    initialOptions.footer = {
      js: umdFooter,
    }
  }

  if (initialOptions.banner) {
    if (initialOptions.banner.js) {
      initialOptions.banner.js += content
    } else {
      initialOptions.banner.js = content
    }
  } else {
    initialOptions.banner = {
      js: content,
    }
  }
}

export const createUmdWrapper = opts => {
  let pluginExternalDependencies = []

  return {
    name: 'add-umd-wrapper',
    esbuildOptions(options) {
      options.format = 'cjs'
      pluginExternalDependencies = []
      return options
    },
    buildEnd(result) {
      try {
        result.writtenFiles.forEach(file => {
          const filePath = path.join(process.cwd(), file.name)
          if (file.name.endsWith('.js')) {
            const fileName = path.basename(file.name)
            const umdBanner = getUmdBanner({ ...opts, external: pluginExternalDependencies })
            // eslint-disable-next-line security/detect-non-literal-fs-filename
            const content = fs.readFileSync(filePath, 'utf-8')
            const patchedFileContents = content.replace(`//# sourceMappingURL=${fileName}.map`, '')
            const scriptContent = `\n\n\n${patchedFileContents}\n\n\n`
            const wrappedContent = `${umdBanner}${scriptContent}${umdFooter}\n\n//# sourceMappingURL=${fileName}.map\n`
            const newContent = `/* umd */\n${wrappedContent}`
            // eslint-disable-next-line security/detect-non-literal-fs-filename
            fs.writeFileSync(filePath, newContent, 'utf8')
          }
        })
      } catch (err) {
        // eslint-disable-next-line no-console
        console.error(err)
      }
    },
  }
}
dhowe commented 10 months ago

thanks @weyert