https://github.com/morganney/vite-plugin-specifier
Update ESM and CJS specifiers during your Vite build.
https://github.com/morganney/vite-plugin-specifier
cjs esm file-extension rollup-plugin specifier vite-plugin
Last synced: 8 months ago
JSON representation
Update ESM and CJS specifiers during your Vite build.
- Host: GitHub
- URL: https://github.com/morganney/vite-plugin-specifier
- Owner: morganney
- License: mit
- Created: 2023-08-06T14:16:53.000Z (almost 3 years ago)
- Default Branch: main
- Last Pushed: 2024-09-18T00:01:14.000Z (almost 2 years ago)
- Last Synced: 2025-10-13T02:47:58.691Z (9 months ago)
- Topics: cjs, esm, file-extension, rollup-plugin, specifier, vite-plugin
- Language: TypeScript
- Homepage:
- Size: 204 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# [`vite-plugin-specifier`](https://www.npmjs.com/package/vite-plugin-specifier)

[](https://codecov.io/gh/morganney/vite-plugin-specifier)
[](https://www.npmjs.com/package/vite-plugin-specifier)
Vite plugin to update your ESM and CJS specifiers.
## Why would I need this?
Maybe you're running vite in [library mode](https://vitejs.dev/guide/build.html#library-mode), or using a plugin like [`vite-plugin-no-bundle`](https://github.com/ManBearTM/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](https://vitejs.dev/guide/build.html#library-mode) in both ESM and CJS [`build.lib.formats`](https://vitejs.dev/config/build-options.html#build-lib), _but use **`.mjs`** extensions for the ESM build_, by defining the following vite.config.ts:
```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](#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:
```diff
+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:
```diff
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`](https://vitejs.dev/config/build-options.html#build-lib). It will do what you expect, i.e. update the relative specifiers and output the declaration files with correct extensions.
## Options
### `hook`
**type**
```ts
type Hook = 'writeBundle' | 'transform'
```
Determines what [vite build hook](https://vitejs.dev/guide/api-plugin.html#universal-hooks) this plugin runs under. By default, this plugin runs after the vite build is finished writing files, during the [`writeBundle`](https://rollupjs.org/plugin-development/#writebundle) hook.
If you run this plugin under [`transform`](https://rollupjs.org/plugin-development/#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:
```ts
resolve: {
alias: [
{
find: /(.+)\.mjs$/,
replacement: '$1.js'
}
]
},
```
### `map`
**type**
```ts
type Map = Record
```
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**
```ts
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 = {
[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**
```ts
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`](https://github.com/knightedcodemonkey/specifier) to get the updated specifier value.
### `writer`
**type**
```ts
type Writer = ((records: BundleRecords) => Promise) | boolean
type BundleRecords = Record
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.