npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

vite-plugin-specifier

v1.0.1

Published

Vite plugin to update your ESM and CJS specifiers.

Downloads

163

Readme

vite-plugin-specifier

CI codecov NPM version

Vite plugin to update your ESM and CJS specifiers.

Why would I need this?

Maybe you're running vite in library mode, or using a plugin like vite-plugin-no-bundle, and you want to be able to change the default specifier and file extensions generated by vite. This plugin allows you to do that using whatever type you want in your package.json.

Example

Given an ESM-first ("type": "module") project with this structure:

.
├── src/
│   ├── index.ts
│   └── file.ts
├── package.json
├── tsconfig.json
└── vite.config.ts

You can build a library in both ESM and CJS build.lib.formats, but use .mjs extensions for the ESM build, by defining the following vite.config.ts:

import { defineConfig } from 'vite'
import specifier from 'vite-plugin-specifier'

export default defineConfig(({
  build: {
    lib: {
      formats: ['es', 'cjs'],
      entry: ['src/index.ts', 'src/file.ts'],
    },
  },
  plugins: [
    specifier({
      extMap: {
        '.js': '.mjs',
      },
    }),
  ],
}))

After running the vite build, all relative specifiers ending in .js would be updated to end in .mjs, and your dist would contain the following:

.
├── dist/
│   ├── index.cjs
│   ├── index.mjs
│   ├── file.cjs
│   └── file.mjs
├── src/
│   ├── index.ts
│   └── file.ts
├── package.json
├── tsconfig.json
└── vite.config.ts

You can do the same for a CJS-first project and change the extensions to .cjs.

If you need more fine-grained control than extMap offers, you can use the handler and writer options to update specifier and file extensions any way you see fit.

Advanced

As an example of how to use handler and writer, they can be used to create the same build as above done with extMap.

The updated vite.config.ts:

+import { writeFile, rm } from 'node:fs/promises'

import { defineConfig } from 'vite'
import specifier from 'vite-plugin-specifier'

export default defineConfig(({
  build: {
    lib: {
      formats: ['cjs', 'es'],
      entry: ['src/index.ts', 'src/file.ts'],
    },
  },
  plugins: [
    specifier({
-      extMap: {
-        '.js': '.mjs',
-      },
+      handler({ value }) {
+        if (value.startsWith('./') || value.startsWith('../')) {
+          return value.replace(/([^.]+)\.js$/, '$1.mjs')
+        }
+      },
+      async writer(records) {
+        const files = Object.keys(records)
+
+        for (const filename of files) {
+          if (typeof records[filename] === 'string' && filename.endsWith('.js')) {
+            await writeFile(filename.replace(/\.js$/, '.mjs'), records[filename])
+            await rm(filename, { force: true })
+          }
+        }
+      },
    }),
  ],
}))

As you can see, it's much simpler to just use extMap which does this for you. However, if you want to modify file extensions and/or specifiers in general (not just relative ones) after a vite build, then handler and writer are what you want.

TypeScript declaration files

You can change file and relative specifier extensions in .d.ts files using the extMap option.

Run tsc first to build your types resulting in the following dist:

.
├── dist/
│   ├── index.d.ts
│   ├── file.d.ts
├── src/
│   ├── index.ts
│   └── file.ts
├── package.json
├── tsconfig.json
└── vite.config.ts

Now update your vite.config.ts to the following:

build: {
+ emptyOutDir: false,
  lib: {
    formats: ['es', 'cjs'],
    entry: ['src/index.ts', 'src/file.ts'],
  },
},
plugins: [
  specifier({
    extMap: {
      '.js': '.mjs',
+     '.d.ts': 'dual'
    },
  }),
],

After running the vite build, the .d.ts files will have been transformed twice, once to update relative specifiers to end with .mjs, and once to end with .cjs. Your dist will now contain the following:

.
├── dist/
│   ├── index.cjs
│   ├── index.d.cts
│   ├── index.d.mts
│   ├── index.mjs
│   ├── file.cjs
│   ├── file.d.cts
│   ├── file.d.mts
│   └── file.mjs
├── src/
│   ├── index.ts
│   └── file.ts
├── package.json
├── tsconfig.json
└── vite.config.ts

Besides the unique value dual, you can also map .d.ts to either .mjs or .cjs if you are not running vite with multiple build.lib.formats. It will do what you expect, i.e. update the relative specifiers and output the declaration files with correct extensions.

Options

hook

type

type Hook = 'writeBundle' | 'transform'

Determines what vite build hook this plugin runs under. By default, this plugin runs after the vite build is finished writing files, during the writeBundle hook.

If you run this plugin under transform, then depending on what you're doing you might need to include some sort of resolve.alias configuration to remap the changed specifier extensions. For example, running the example above under transform would require this added to the vite.config.ts:

resolve: {
  alias: [
    {
      find: /(.+)\.mjs$/,
      replacement: '$1.js'
    }
  ]
},

map

type

type Map = Record<string, string>

An object that maps one string to another. If any specifier matches a map's key, the corresponding value will be used to update the specifier.

extMap

type

type ExtMap = Map<{
  '.js': '.mjs' | '.cjs'
  '.mjs': '.js'
  '.cjs': '.js'
  '.jsx': '.js' | '.mjs' | '.cjs'
  '.ts': '.js' | '.mjs' | '.cjs'
  '.mts': '.mjs' | '.js'
  '.cts': '.cjs' | '.js'
  '.tsx': '.js' | '.mjs' | '.cjs'
  '.d.ts': '.d.mts' | '.d.cts' | 'dual'
}>
type Map<Exts> = {
  [P in keyof Exts]?: Exts[P]
}

An object of common file extensions mapping one extension to another. Using this option allows you to easily change one extension into another for relative specifiers and their associated files.

handler

type

type Handler = Callback | RegexMap
type Callback = (spec: Spec) => string
interface RegexMap {
  [regex: string]: string
}
interface Spec {
  type: 'StringLiteral' | 'TemplateLiteral' | 'BinaryExpression' | 'NewExpression'
  start: number
  end: number
  value: string
  loc: SourceLocation
}

Allows updating of specifiers on a per-file basis, using a callback or regular expression map to determine the updated specifier values. The Spec used in the callback is essentially a portion of an AST node. The handler is passed to @knighted/specifier to get the updated specifier value.

writer

type

type Writer = ((records: BundleRecords) => Promise<void>) | boolean
type BundleRecords = Record<string, { error: UpdateError | undefined; code: string }>
interface UpdateError {
  error: boolean
  msg: string
  filename?: string
  syntaxError?: {
    code: string
    reasonCode: string
  }
}

Used to modify the emitted build files, for instance to change their file extensions. Receives a BundleRecords object mapping the filenames from the emitted build, to their updated source code string, or an object describing an error that occured.

Setting this option to true will use a default writer that writes the updated source code back to the original filename.