vite-plugin-specifier
TypeScript icon, indicating that this package has built-in type declarations

1.0.1 • Public • Published

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.

Package Sidebar

Install

npm i vite-plugin-specifier

Weekly Downloads

6

Version

1.0.1

License

MIT

Unpacked Size

24.5 kB

Total Files

7

Last publish

Collaborators

  • morganney