{"id":22808931,"url":"https://github.com/morganney/vite-plugin-specifier","last_synced_at":"2025-10-16T10:22:40.567Z","repository":{"id":186521130,"uuid":"675315696","full_name":"morganney/vite-plugin-specifier","owner":"morganney","description":"Update ESM and CJS specifiers during your Vite build.","archived":false,"fork":false,"pushed_at":"2024-09-18T00:01:14.000Z","size":209,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-13T02:47:58.691Z","etag":null,"topics":["cjs","esm","file-extension","rollup-plugin","specifier","vite-plugin"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/morganney.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-08-06T14:16:53.000Z","updated_at":"2023-08-12T04:45:44.000Z","dependencies_parsed_at":null,"dependency_job_id":"e4264b8f-1278-459c-b9b3-1def73ec48fe","html_url":"https://github.com/morganney/vite-plugin-specifier","commit_stats":null,"previous_names":["morganney/vite-plugin-specifier"],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/morganney/vite-plugin-specifier","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morganney%2Fvite-plugin-specifier","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morganney%2Fvite-plugin-specifier/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morganney%2Fvite-plugin-specifier/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morganney%2Fvite-plugin-specifier/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/morganney","download_url":"https://codeload.github.com/morganney/vite-plugin-specifier/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/morganney%2Fvite-plugin-specifier/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279178018,"owners_count":26120090,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-16T02:00:06.019Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cjs","esm","file-extension","rollup-plugin","specifier","vite-plugin"],"created_at":"2024-12-12T11:12:53.173Z","updated_at":"2025-10-16T10:22:40.534Z","avatar_url":"https://github.com/morganney.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# [`vite-plugin-specifier`](https://www.npmjs.com/package/vite-plugin-specifier)\n\n![CI](https://github.com/morganney/vite-plugin-specifier/actions/workflows/ci.yml/badge.svg)\n[![codecov](https://codecov.io/gh/morganney/vite-plugin-specifier/branch/main/graph/badge.svg?token=HGIBWFXW98)](https://codecov.io/gh/morganney/vite-plugin-specifier)\n[![NPM version](https://img.shields.io/npm/v/vite-plugin-specifier.svg)](https://www.npmjs.com/package/vite-plugin-specifier)\n\nVite plugin to update your ESM and CJS specifiers.\n\n## Why would I need this?\n\nMaybe 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.\n\n## Example\n\nGiven an ESM-first (`\"type\": \"module\"`) project with this structure:\n\n```\n.\n├── src/\n│   ├── index.ts\n│   └── file.ts\n├── package.json\n├── tsconfig.json\n└── vite.config.ts\n```\n\nYou 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:\n\n```ts\nimport { defineConfig } from 'vite'\nimport specifier from 'vite-plugin-specifier'\n\nexport default defineConfig(({\n  build: {\n    lib: {\n      formats: ['es', 'cjs'],\n      entry: ['src/index.ts', 'src/file.ts'],\n    },\n  },\n  plugins: [\n    specifier({\n      extMap: {\n        '.js': '.mjs',\n      },\n    }),\n  ],\n}))\n```\n\nAfter running the vite build, all **relative** specifiers ending in `.js` would be updated to end in `.mjs`, and your `dist` would contain the following:\n\n```\n.\n├── dist/\n│   ├── index.cjs\n│   ├── index.mjs\n│   ├── file.cjs\n│   └── file.mjs\n├── src/\n│   ├── index.ts\n│   └── file.ts\n├── package.json\n├── tsconfig.json\n└── vite.config.ts\n```\n\nYou can do the same for a CJS-first project and change the extensions to `.cjs`.\n\nIf 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.\n\n### Advanced\n\nAs an example of how to use `handler` and `writer`, they can be used to create the same build as above done with `extMap`.\n\nThe updated vite.config.ts:\n\n```diff\n+import { writeFile, rm } from 'node:fs/promises'\n\nimport { defineConfig } from 'vite'\nimport specifier from 'vite-plugin-specifier'\n\nexport default defineConfig(({\n  build: {\n    lib: {\n      formats: ['cjs', 'es'],\n      entry: ['src/index.ts', 'src/file.ts'],\n    },\n  },\n  plugins: [\n    specifier({\n-      extMap: {\n-        '.js': '.mjs',\n-      },\n+      handler({ value }) {\n+        if (value.startsWith('./') || value.startsWith('../')) {\n+          return value.replace(/([^.]+)\\.js$/, '$1.mjs')\n+        }\n+      },\n+      async writer(records) {\n+        const files = Object.keys(records)\n+\n+        for (const filename of files) {\n+          if (typeof records[filename] === 'string' \u0026\u0026 filename.endsWith('.js')) {\n+            await writeFile(filename.replace(/\\.js$/, '.mjs'), records[filename])\n+            await rm(filename, { force: true })\n+          }\n+        }\n+      },\n    }),\n  ],\n}))\n```\n\nAs 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.\n\n### TypeScript declaration files\n\nYou can change file and relative specifier extensions in `.d.ts` files using the `extMap` option.\n\nRun `tsc` first to build your types resulting in the following `dist`:\n\n```\n.\n├── dist/\n│   ├── index.d.ts\n│   ├── file.d.ts\n├── src/\n│   ├── index.ts\n│   └── file.ts\n├── package.json\n├── tsconfig.json\n└── vite.config.ts\n```\n\nNow update your vite.config.ts to the following:\n\n```diff\nbuild: {\n+ emptyOutDir: false,\n  lib: {\n    formats: ['es', 'cjs'],\n    entry: ['src/index.ts', 'src/file.ts'],\n  },\n},\nplugins: [\n  specifier({\n    extMap: {\n      '.js': '.mjs',\n+     '.d.ts': 'dual'\n    },\n  }),\n],\n```\n\nAfter 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:\n\n```\n.\n├── dist/\n│   ├── index.cjs\n│   ├── index.d.cts\n│   ├── index.d.mts\n│   ├── index.mjs\n│   ├── file.cjs\n│   ├── file.d.cts\n│   ├── file.d.mts\n│   └── file.mjs\n├── src/\n│   ├── index.ts\n│   └── file.ts\n├── package.json\n├── tsconfig.json\n└── vite.config.ts\n```\n\nBesides 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.\n\n\n## Options\n\n### `hook`\n\n**type**\n```ts\ntype Hook = 'writeBundle' | 'transform'\n```\n\nDetermines 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.\n\nIf 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:\n\n```ts\nresolve: {\n  alias: [\n    {\n      find: /(.+)\\.mjs$/,\n      replacement: '$1.js'\n    }\n  ]\n},\n```\n\n### `map`\n\n**type**\n```ts\ntype Map = Record\u003cstring, string\u003e\n```\n\nAn 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.\n\n### `extMap`\n\n**type**\n```ts\ntype ExtMap = Map\u003c{\n  '.js': '.mjs' | '.cjs'\n  '.mjs': '.js'\n  '.cjs': '.js'\n  '.jsx': '.js' | '.mjs' | '.cjs'\n  '.ts': '.js' | '.mjs' | '.cjs'\n  '.mts': '.mjs' | '.js'\n  '.cts': '.cjs' | '.js'\n  '.tsx': '.js' | '.mjs' | '.cjs'\n  '.d.ts': '.d.mts' | '.d.cts' | 'dual'\n}\u003e\ntype Map\u003cExts\u003e = {\n  [P in keyof Exts]?: Exts[P]\n}\n```\n\nAn 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**.\n\n\n### `handler`\n\n**type**\n```ts\ntype Handler = Callback | RegexMap\ntype Callback = (spec: Spec) =\u003e string\ninterface RegexMap {\n  [regex: string]: string\n}\ninterface Spec {\n  type: 'StringLiteral' | 'TemplateLiteral' | 'BinaryExpression' | 'NewExpression'\n  start: number\n  end: number\n  value: string\n  loc: SourceLocation\n}\n```\n\nAllows 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.\n\n### `writer`\n\n**type**\n```ts\ntype Writer = ((records: BundleRecords) =\u003e Promise\u003cvoid\u003e) | boolean\ntype BundleRecords = Record\u003cstring, { error: UpdateError | undefined; code: string }\u003e\ninterface UpdateError {\n  error: boolean\n  msg: string\n  filename?: string\n  syntaxError?: {\n    code: string\n    reasonCode: string\n  }\n}\n```\n\nUsed 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.\n\nSetting this option to `true` will use a default writer that writes the updated source code back to the original filename.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmorganney%2Fvite-plugin-specifier","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmorganney%2Fvite-plugin-specifier","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmorganney%2Fvite-plugin-specifier/lists"}