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 for 'umd' and 'amd' formats #924

Open
bo-carey opened this issue Jun 13, 2023 · 8 comments
Open

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

bo-carey opened this issue Jun 13, 2023 · 8 comments

Comments

@bo-carey
Copy link

bo-carey commented Jun 13, 2023

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

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar
@weyert
Copy link

weyert commented Aug 12, 2023

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
Copy link

dhowe commented Nov 11, 2023

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

@dhowe
Copy link

dhowe commented Nov 11, 2023

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

@bo-carey
Copy link
Author

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
Copy link

dhowe commented Nov 13, 2023

thanks @bo-carey

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

with what tooling ?

@bo-carey
Copy link
Author

bo-carey commented Nov 13, 2023

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
Copy link

weyert commented Nov 13, 2023

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
Copy link

dhowe commented Nov 13, 2023

thanks @weyert

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

3 participants