diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index eb21a63..c2f8518 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -21,6 +21,8 @@ body:
[plugin-jsx-remove-attributes](https://github.com/rolldown/plugins/tree/main/packages/jsx-remove-attributes)
- label: |-
[plugin-styled-jsx](https://github.com/rolldown/plugins/tree/main/packages/styled-jsx)
+ - label: |-
+ [plugin-transform-imports](https://github.com/rolldown/plugins/tree/main/packages/transform-imports)
- type: textarea
id: bug-description
attributes:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index bc37b74..ac3921b 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -21,6 +21,8 @@ body:
[plugin-jsx-remove-attributes](https://github.com/rolldown/plugins/tree/main/packages/jsx-remove-attributes)
- label: |-
[plugin-styled-jsx](https://github.com/rolldown/plugins/tree/main/packages/styled-jsx)
+ - label: |-
+ [plugin-transform-imports](https://github.com/rolldown/plugins/tree/main/packages/transform-imports)
- type: textarea
id: feature-description
attributes:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 6de7aa1..2785c4a 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -6,3 +6,5 @@
- '/- \[x\] \[plugin-jsx-remove-attributes\]/i'
'plugin: styled-jsx':
- '/- \[x\] \[plugin-styled-jsx\]/i'
+'plugin: transform-imports':
+ - '/- \[x\] \[plugin-transform-imports\]/i'
diff --git a/.github/pr-labeler.yml b/.github/pr-labeler.yml
index f62a370..f6ef885 100644
--- a/.github/pr-labeler.yml
+++ b/.github/pr-labeler.yml
@@ -14,6 +14,10 @@
- changed-files:
- any-glob-to-any-file: 'packages/styled-jsx/**'
+'plugin: transform-imports':
+ - changed-files:
+ - any-glob-to-any-file: 'packages/transform-imports/**'
+
'package: oxc-unshadowed-visitor':
- changed-files:
- any-glob-to-any-file: 'packages/oxc-unshadowed-visitor/**'
diff --git a/README.md b/README.md
index 8ca796a..3acb906 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ Official Rolldown plugins
- [`@rolldown/plugin-emotion`](https://github.com/rolldown/plugins/tree/main/packages/emotion) ([![NPM version][badge-npm-version-emotion]][url-npm-emotion]): minification and optimization of Emotion styles
- [`@rolldown/plugin-jsx-remove-attributes`](https://github.com/rolldown/plugins/tree/main/packages/jsx-remove-attributes) ([![NPM version][badge-npm-version-jsx-remove-attributes]][url-npm-jsx-remove-attributes]): remove JSX attributes (e.g. data-testid)
- [`@rolldown/plugin-styled-jsx`](https://github.com/rolldown/plugins/tree/main/packages/styled-jsx) ([![NPM version][badge-npm-version-styled-jsx]][url-npm-styled-jsx]): Rolldown plugin for styled-jsx CSS scoping
+- [`@rolldown/plugin-transform-imports`](https://github.com/rolldown/plugins/tree/main/packages/transform-imports) ([![NPM version][badge-npm-version-transform-imports]][url-npm-transform-imports]): transform imports/exports to barrel files
### Other Packages
@@ -38,9 +39,11 @@ Official Rolldown plugins
[badge-npm-version-emotion]: https://img.shields.io/npm/v/@rolldown/plugin-emotion?color=brightgreen
[badge-npm-version-jsx-remove-attributes]: https://img.shields.io/npm/v/@rolldown/plugin-jsx-remove-attributes?color=brightgreen
[badge-npm-version-styled-jsx]: https://img.shields.io/npm/v/@rolldown/plugin-styled-jsx?color=brightgreen
+[badge-npm-version-transform-imports]: https://img.shields.io/npm/v/@rolldown/plugin-transform-imports?color=brightgreen
[badge-npm-version-oxc-unshadowed-visitor]: https://img.shields.io/npm/v/oxc-unshadowed-visitor?color=brightgreen
[url-npm-babel]: https://npmx.dev/package/@rolldown/plugin-babel
[url-npm-emotion]: https://npmx.dev/package/@rolldown/plugin-emotion
[url-npm-jsx-remove-attributes]: https://npmx.dev/package/@rolldown/plugin-jsx-remove-attributes
[url-npm-styled-jsx]: https://npmx.dev/package/@rolldown/plugin-styled-jsx
+[url-npm-transform-imports]: https://npmx.dev/package/@rolldown/plugin-transform-imports
[url-npm-oxc-unshadowed-visitor]: https://npmx.dev/package/oxc-unshadowed-visitor
diff --git a/examples/transform-imports/index.html b/examples/transform-imports/index.html
new file mode 100644
index 0000000..0b5a3c1
--- /dev/null
+++ b/examples/transform-imports/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Transform Imports Example
+
+
+
+
+
+
diff --git a/examples/transform-imports/package.json b/examples/transform-imports/package.json
new file mode 100644
index 0000000..b48a118
--- /dev/null
+++ b/examples/transform-imports/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@rolldown/example-transform-imports",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "@rolldown/plugin-transform-imports": "workspace:*",
+ "vite": "^8.0.7"
+ }
+}
diff --git a/examples/transform-imports/src/main.ts b/examples/transform-imports/src/main.ts
new file mode 100644
index 0000000..81a3328
--- /dev/null
+++ b/examples/transform-imports/src/main.ts
@@ -0,0 +1,19 @@
+// @ts-expect-error not typed
+import { Button, Card, Modal } from 'mock-lib'
+
+const app = document.getElementById('app')!
+
+const heading = document.createElement('h1')
+heading.textContent = 'Transform Imports Example'
+heading.className = 'app-title'
+app.appendChild(heading)
+
+const description = document.createElement('p')
+description.textContent =
+ 'These components are imported via barrel import, transformed to individual imports by the plugin.'
+description.className = 'app-description'
+app.appendChild(description)
+
+app.appendChild(Button())
+app.appendChild(Card())
+app.appendChild(Modal())
diff --git a/examples/transform-imports/src/mock-lib/button.ts b/examples/transform-imports/src/mock-lib/button.ts
new file mode 100644
index 0000000..8c69cb3
--- /dev/null
+++ b/examples/transform-imports/src/mock-lib/button.ts
@@ -0,0 +1,6 @@
+export default function Button() {
+ const el = document.createElement('button')
+ el.textContent = 'Button'
+ el.className = 'mock-button'
+ return el
+}
diff --git a/examples/transform-imports/src/mock-lib/card.ts b/examples/transform-imports/src/mock-lib/card.ts
new file mode 100644
index 0000000..60b123b
--- /dev/null
+++ b/examples/transform-imports/src/mock-lib/card.ts
@@ -0,0 +1,6 @@
+export default function Card() {
+ const el = document.createElement('div')
+ el.textContent = 'Card'
+ el.className = 'mock-card'
+ return el
+}
diff --git a/examples/transform-imports/src/mock-lib/index.ts b/examples/transform-imports/src/mock-lib/index.ts
new file mode 100644
index 0000000..909ddca
--- /dev/null
+++ b/examples/transform-imports/src/mock-lib/index.ts
@@ -0,0 +1,5 @@
+export { default as Button } from './button.js'
+export { default as Input } from './input.js'
+export { default as Select } from './select.js'
+export { default as Modal } from './modal.js'
+export { default as Card } from './card.js'
diff --git a/examples/transform-imports/src/mock-lib/input.ts b/examples/transform-imports/src/mock-lib/input.ts
new file mode 100644
index 0000000..14169ef
--- /dev/null
+++ b/examples/transform-imports/src/mock-lib/input.ts
@@ -0,0 +1,6 @@
+export default function Input() {
+ const el = document.createElement('input')
+ el.placeholder = 'Input'
+ el.className = 'mock-input'
+ return el
+}
diff --git a/examples/transform-imports/src/mock-lib/modal.ts b/examples/transform-imports/src/mock-lib/modal.ts
new file mode 100644
index 0000000..1d6d27e
--- /dev/null
+++ b/examples/transform-imports/src/mock-lib/modal.ts
@@ -0,0 +1,6 @@
+export default function Modal() {
+ const el = document.createElement('div')
+ el.textContent = 'Modal'
+ el.className = 'mock-modal'
+ return el
+}
diff --git a/examples/transform-imports/src/mock-lib/select.ts b/examples/transform-imports/src/mock-lib/select.ts
new file mode 100644
index 0000000..2fc101a
--- /dev/null
+++ b/examples/transform-imports/src/mock-lib/select.ts
@@ -0,0 +1,8 @@
+export default function Select() {
+ const el = document.createElement('select')
+ el.className = 'mock-select'
+ const option = document.createElement('option')
+ option.textContent = 'Select'
+ el.appendChild(option)
+ return el
+}
diff --git a/examples/transform-imports/transform-imports.test.ts b/examples/transform-imports/transform-imports.test.ts
new file mode 100644
index 0000000..a83c113
--- /dev/null
+++ b/examples/transform-imports/transform-imports.test.ts
@@ -0,0 +1,20 @@
+import { expect, test } from 'vitest'
+import { page } from '~utils'
+
+test('should render button component', async () => {
+ const el = page.locator('.mock-button')
+ const text = await el.textContent()
+ expect(text).toBe('Button')
+})
+
+test('should render card component', async () => {
+ const el = page.locator('.mock-card')
+ const text = await el.textContent()
+ expect(text).toBe('Card')
+})
+
+test('should render modal component', async () => {
+ const el = page.locator('.mock-modal')
+ const text = await el.textContent()
+ expect(text).toBe('Modal')
+})
diff --git a/examples/transform-imports/vite.config.ts b/examples/transform-imports/vite.config.ts
new file mode 100644
index 0000000..7f1c871
--- /dev/null
+++ b/examples/transform-imports/vite.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from 'vite'
+import transformImports from '@rolldown/plugin-transform-imports'
+import path from 'node:path'
+
+export default defineConfig({
+ plugins: [
+ transformImports({
+ modules: {
+ 'mock-lib': {
+ transform: 'mock-lib/{{kebabCase member}}',
+ },
+ },
+ }),
+ ],
+ resolve: {
+ alias: {
+ 'mock-lib': path.resolve(import.meta.dirname, './src/mock-lib'),
+ },
+ },
+})
diff --git a/internal-packages/swc-output-gen/package.json b/internal-packages/swc-output-gen/package.json
index 04fe36b..fe3a6ea 100644
--- a/internal-packages/swc-output-gen/package.json
+++ b/internal-packages/swc-output-gen/package.json
@@ -13,6 +13,7 @@
"@swc/plugin-emotion": "^14.8.0",
"@swc/plugin-react-remove-properties": "^12.8.0",
"@swc/plugin-styled-jsx": "^13.8.0",
+ "@swc/plugin-transform-imports": "^12.8.0",
"rolldown": "^1.0.0-rc.13",
"tinyglobby": "^0.2.15"
}
diff --git a/internal-packages/swc-output-gen/src/plugin-registry.ts b/internal-packages/swc-output-gen/src/plugin-registry.ts
index e9cc0f7..9bc0753 100644
--- a/internal-packages/swc-output-gen/src/plugin-registry.ts
+++ b/internal-packages/swc-output-gen/src/plugin-registry.ts
@@ -46,6 +46,17 @@ export const pluginRegistry: Record = {
return [['@swc/plugin-styled-jsx', swcConfig]]
},
},
+ 'transform-imports': {
+ packages: ['@swc/plugin-transform-imports'],
+ mapOptions: (config) => [['@swc/plugin-transform-imports', config]],
+ shouldSkip: (config) => {
+ // SWC plugin only supports camelCase and kebabCase helpers
+ // Our ported plugin also supports snakeCase, lowerCase, upperCase
+ const unsupportedHelpers = ['snakeCase', 'lowerCase', 'upperCase']
+ const configStr = JSON.stringify(config)
+ return unsupportedHelpers.some((helper) => configStr.includes(helper))
+ },
+ },
}
/** Get list of all supported plugin names */
diff --git a/packages/transform-imports/CHANGELOG.md b/packages/transform-imports/CHANGELOG.md
new file mode 100644
index 0000000..e69de29
diff --git a/packages/transform-imports/README.md b/packages/transform-imports/README.md
new file mode 100644
index 0000000..dad4f13
--- /dev/null
+++ b/packages/transform-imports/README.md
@@ -0,0 +1,226 @@
+# @rolldown/plugin-transform-imports [](https://npmx.dev/package/@rolldown/plugin-transform-imports)
+
+Rolldown plugin for transforming imports and re-exports from barrel files into individual module imports. This improves performance by avoiding resolving all of imports in the barrel file. For example, it rewrites `import { map } from "lodash"` with `import map from "lodash/map"`.
+
+## Install
+
+```bash
+pnpm add -D @rolldown/plugin-transform-imports
+```
+
+## Usage
+
+```js
+import transformImports from '@rolldown/plugin-transform-imports'
+
+export default {
+ plugins: [
+ transformImports({
+ modules: {
+ lodash: {
+ transform: 'lodash/{{member}}',
+ },
+ },
+ }),
+ ],
+}
+```
+
+This transforms:
+
+```js
+import { map, filter } from 'lodash'
+```
+
+Into:
+
+```js
+import map from 'lodash/map'
+import filter from 'lodash/filter'
+```
+
+## Options
+
+### `modules`
+
+- **Type:** `Record`
+
+A map of module names (or regex patterns) to their transform configuration.
+
+Module names are converted to regex patterns: `lodash` matches exactly `lodash`, while `^lodash(/.*)?$` allows custom regex patterns (must start with `^` and end with `$`).
+
+## TransformConfig
+
+### `transform`
+
+- **Type:** `string | [pattern: string, template: string][]`
+- **Required**
+
+The path template for transformed imports. Use `{{member}}` as a placeholder for the imported member name.
+
+**String form:**
+
+```js
+{
+ transform: 'lodash/{{member}}'
+}
+// import { map } from 'lodash' → import map from 'lodash/map'
+```
+
+**Array of tuples form** for pattern-based routing:
+
+```js
+{
+ transform: [
+ ['someFunc', 'some-lib/some-module'],
+ ['*', 'some-lib/{{member}}'],
+ ]
+}
+```
+
+Each tuple is `[pattern, template]`. Patterns are treated as regex (except `*` which matches anything). The first matching pattern wins.
+
+### `preventFullImport`
+
+- **Type:** `boolean`
+- **Default:** `false`
+
+When `true`, throws an error on namespace imports (`import * as X`) and side-effect imports (`import 'mod'`).
+
+### `skipDefaultConversion`
+
+- **Type:** `boolean`
+- **Default:** `false`
+
+When `true`, keeps the import as a named import instead of converting to a default import:
+
+```js
+// skipDefaultConversion: false (default)
+import { map } from 'lodash'
+import map from 'lodash/map' // output
+
+// skipDefaultConversion: true
+import { map } from 'lodash'
+import { map } from 'lodash/map' // output
+```
+
+### `handleDefaultImport`
+
+- **Type:** `boolean`
+- **Default:** `false`
+
+When `true`, transforms default imports using the local name as the member:
+
+```js
+// handleDefaultImport: true
+import myMap from 'lodash'
+import myMap from 'lodash/myMap' // output
+```
+
+### `handleNamespaceImport`
+
+- **Type:** `boolean`
+- **Default:** `false`
+
+When `true`, transforms namespace imports using the local name as the member:
+
+```js
+// handleNamespaceImport: true
+import * as myMap from 'lodash'
+import * as myMap from 'lodash/myMap' // output
+```
+
+## Template Syntax
+
+Templates use `{{...}}` placeholders with the following variables:
+
+### Variables
+
+- **`{{member}}`** — the imported member name
+- **`{{matches.[N]}}`** — capture group from the module name regex
+- **`{{memberMatches.[N]}}`** — capture group from the member pattern (tuple form only)
+
+### Case Transforms
+
+Apply a case transform by prefixing the variable:
+
+- **`{{camelCase member}}`** — `MyComponent` → `myComponent`
+- **`{{kebabCase member}}`** — `MyComponent` → `my-component`
+- **`{{snakeCase member}}`** — `MyComponent` → `my_component`
+- **`{{upperCase member}}`** — `map` → `MAP`
+
+### Example with regex capture groups
+
+```js
+{
+ modules: {
+ '^my-lib/(.+)$': {
+ transform: 'my-lib/dist/{{matches.[1]}}/{{kebabCase member}}',
+ },
+ },
+}
+// import { MyButton } from 'my-lib/components'
+// → import MyButton from 'my-lib/dist/components/my-button'
+```
+
+## Re-exports
+
+The plugin also transforms re-exports:
+
+```js
+// input
+export { map, filter } from 'lodash'
+// output
+export { default as map } from 'lodash/map'
+export { default as filter } from 'lodash/filter'
+```
+
+With `skipDefaultConversion: true`:
+
+```js
+// input
+export { map, filter } from 'lodash'
+// output
+export { map } from 'lodash/map'
+export { filter } from 'lodash/filter'
+```
+
+## Dynamic Imports
+
+Static dynamic imports are also transformed:
+
+```js
+const mod = await import('lodash')
+// source is rewritten based on the transform template
+```
+
+## Benchmark
+
+Results of the benchmark that can be run by `pnpm bench` in `./benchmark` directory:
+
+```
+ name hz min max mean p75 p99 p995 p999 rme samples
+· @rolldown/plugin-transform-imports 14.9081 65.9831 69.8849 67.0775 67.5907 69.8849 69.8849 69.8849 ±1.29% 10
+· babel-plugin-transform-imports 5.1254 184.90 246.64 195.11 191.14 246.64 246.64 246.64 ±6.84% 10
+· @swc/plugin-transform-imports 10.9555 88.5649 98.1298 91.2780 92.9843 98.1298 98.1298 98.1298 ±2.50% 10
+
+@rolldown/plugin-transform-imports - bench/transform-imports.bench.ts > Transform Imports Benchmark
+ 1.36x faster than @swc/plugin-transform-imports
+ 2.91x faster than babel-plugin-transform-imports
+```
+
+The benchmark was ran on the following environment:
+
+```
+OS: macOS Tahoe 26.3
+CPU: Apple M4
+Memory: LPDDR5X-7500 32GB
+```
+
+## License
+
+MIT
+
+## Credits
+
+The implementation is based on [swc-project/plugins/packages/transform-imports](https://github.com/swc-project/plugins/tree/main/packages/transform-imports) ([Apache License 2.0](https://github.com/swc-project/plugins/blob/main/LICENSE)). Test cases are also adapted from it.
diff --git a/packages/transform-imports/benchmark/.gitignore b/packages/transform-imports/benchmark/.gitignore
new file mode 100644
index 0000000..d2b9029
--- /dev/null
+++ b/packages/transform-imports/benchmark/.gitignore
@@ -0,0 +1,9 @@
+# Build outputs
+dist/
+
+# Generated modules (regenerated with pnpm generate)
+shared-app/src/index.js
+shared-app/src/modules/
+
+# SWC plugin cache
+.swc/
diff --git a/packages/transform-imports/benchmark/bench/transform-imports.bench.ts b/packages/transform-imports/benchmark/bench/transform-imports.bench.ts
new file mode 100644
index 0000000..352d5e1
--- /dev/null
+++ b/packages/transform-imports/benchmark/bench/transform-imports.bench.ts
@@ -0,0 +1,52 @@
+import { bench, describe } from 'vitest'
+import { execSync } from 'node:child_process'
+import { existsSync, rmSync } from 'node:fs'
+import { resolve } from 'node:path'
+
+const baseDir = resolve(import.meta.dirname, '..')
+const distBase = resolve(baseDir, 'dist')
+const modulesDir = resolve(baseDir, 'shared-app/src/modules')
+
+if (!existsSync(modulesDir)) {
+ execSync('pnpm generate', { cwd: baseDir, stdio: 'inherit' })
+}
+
+function cleanDist(name: string) {
+ const dir = resolve(distBase, name)
+ if (existsSync(dir)) {
+ rmSync(dir, { recursive: true })
+ }
+}
+
+function runBuild(name: string) {
+ execSync(`rolldown -c configs/${name}.ts`, {
+ cwd: baseDir,
+ stdio: 'pipe',
+ })
+}
+
+describe('Transform Imports Benchmark', () => {
+ bench(
+ '@rolldown/plugin-transform-imports',
+ () => {
+ runBuild('custom')
+ },
+ { teardown: () => cleanDist('custom') },
+ )
+
+ bench(
+ 'babel-plugin-transform-imports',
+ () => {
+ runBuild('babel')
+ },
+ { teardown: () => cleanDist('babel') },
+ )
+
+ bench(
+ '@swc/plugin-transform-imports',
+ () => {
+ runBuild('swc')
+ },
+ { teardown: () => cleanDist('swc') },
+ )
+})
diff --git a/packages/transform-imports/benchmark/configs/babel.ts b/packages/transform-imports/benchmark/configs/babel.ts
new file mode 100644
index 0000000..3ac3272
--- /dev/null
+++ b/packages/transform-imports/benchmark/configs/babel.ts
@@ -0,0 +1,41 @@
+import { defineConfig } from 'rolldown'
+import { resolve } from 'node:path'
+import babel, { defineRolldownBabelPreset } from '@rolldown/plugin-babel'
+
+// babel-plugin-transform-imports uses ${member} syntax instead of {{member}}
+// and camelCase/kebabCase are provided as function options, not template helpers.
+// The Babel plugin supports a `transform` function in JS config.
+const transformImportsPreset = defineRolldownBabelPreset({
+ preset: () => ({
+ plugins: [
+ [
+ 'babel-plugin-transform-imports',
+ {
+ 'ui-components': { transform: 'ui-components/lib/${member}', preventFullImport: false },
+ 'utils-lib': { transform: 'utils-lib/${member}', preventFullImport: false },
+ 'icons-pack': { transform: 'icons-pack/icons/${member}', preventFullImport: false },
+ 'data-helpers': { transform: 'data-helpers/${member}', preventFullImport: false },
+ 'form-controls': { transform: 'form-controls/lib/${member}', preventFullImport: false },
+ },
+ ],
+ ],
+ }),
+ rolldown: {
+ filter: {
+ id: { include: /\.[jt]sx?$/, exclude: /node_modules/ },
+ },
+ },
+})
+
+export default defineConfig({
+ input: resolve(import.meta.dirname, '../shared-app/src/index.js'),
+ external: [/^ui-components/, /^utils-lib/, /^icons-pack/, /^data-helpers/, /^form-controls/],
+ output: {
+ dir: resolve(import.meta.dirname, '../dist/babel'),
+ },
+ plugins: [
+ babel({
+ presets: [transformImportsPreset],
+ }),
+ ],
+})
diff --git a/packages/transform-imports/benchmark/configs/custom.ts b/packages/transform-imports/benchmark/configs/custom.ts
new file mode 100644
index 0000000..2c03471
--- /dev/null
+++ b/packages/transform-imports/benchmark/configs/custom.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from 'rolldown'
+import { resolve } from 'node:path'
+import transformImports from '@rolldown/plugin-transform-imports'
+
+const modules = {
+ 'ui-components': { transform: 'ui-components/lib/{{kebabCase member}}' },
+ 'utils-lib': { transform: 'utils-lib/{{camelCase member}}' },
+ 'icons-pack': { transform: 'icons-pack/icons/{{kebabCase member}}' },
+ 'data-helpers': { transform: 'data-helpers/{{member}}' },
+ 'form-controls': { transform: 'form-controls/lib/{{kebabCase member}}' },
+}
+
+export default defineConfig({
+ input: resolve(import.meta.dirname, '../shared-app/src/index.js'),
+ external: [/^ui-components/, /^utils-lib/, /^icons-pack/, /^data-helpers/, /^form-controls/],
+ output: {
+ dir: resolve(import.meta.dirname, '../dist/custom'),
+ },
+ plugins: [transformImports({ modules })],
+})
diff --git a/packages/transform-imports/benchmark/configs/swc.ts b/packages/transform-imports/benchmark/configs/swc.ts
new file mode 100644
index 0000000..2c7d527
--- /dev/null
+++ b/packages/transform-imports/benchmark/configs/swc.ts
@@ -0,0 +1,44 @@
+import { defineConfig } from 'rolldown'
+import { resolve } from 'node:path'
+import { withFilter } from 'rolldown/filter'
+import swc from '@rollup/plugin-swc'
+
+export default defineConfig({
+ input: resolve(import.meta.dirname, '../shared-app/src/index.js'),
+ external: [/^ui-components/, /^utils-lib/, /^icons-pack/, /^data-helpers/, /^form-controls/],
+ output: {
+ dir: resolve(import.meta.dirname, '../dist/swc'),
+ },
+ plugins: [
+ withFilter(
+ swc({
+ swc: {
+ jsc: {
+ parser: {
+ syntax: 'ecmascript',
+ },
+ experimental: {
+ plugins: [
+ [
+ '@swc/plugin-transform-imports',
+ {
+ 'ui-components': { transform: 'ui-components/lib/{{ kebabCase member }}' },
+ 'utils-lib': { transform: 'utils-lib/{{ camelCase member }}' },
+ 'icons-pack': { transform: 'icons-pack/icons/{{ kebabCase member }}' },
+ 'data-helpers': { transform: 'data-helpers/{{ member }}' },
+ 'form-controls': { transform: 'form-controls/lib/{{ kebabCase member }}' },
+ },
+ ],
+ ],
+ },
+ },
+ },
+ }),
+ {
+ transform: {
+ id: { include: /\.[jt]sx?$/, exclude: /node_modules/ },
+ },
+ },
+ ),
+ ],
+})
diff --git a/packages/transform-imports/benchmark/package.json b/packages/transform-imports/benchmark/package.json
new file mode 100644
index 0000000..7ec3fc5
--- /dev/null
+++ b/packages/transform-imports/benchmark/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "@rolldown/benchmark-transform-imports",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "generate": "oxnode scripts/generate-app.ts",
+ "bench": "vitest bench --run",
+ "build:custom": "rolldown -c configs/custom.ts",
+ "build:babel": "rolldown -c configs/babel.ts",
+ "build:swc": "rolldown -c configs/swc.ts"
+ },
+ "devDependencies": {
+ "@oxc-node/cli": "^0.1.0",
+ "@rolldown/benchmark-utils": "workspace:*",
+ "@rolldown/plugin-babel": "file:../../babel",
+ "@rolldown/plugin-transform-imports": "workspace:*",
+ "@rollup/plugin-swc": "^0.4.0",
+ "@swc/core": "^1.15.24",
+ "@swc/plugin-transform-imports": "^12.8.0",
+ "@types/node": "^24.12.2",
+ "babel-plugin-transform-imports": "^2.0.0",
+ "rolldown": "^1.0.0-rc.13"
+ }
+}
diff --git a/packages/transform-imports/benchmark/scripts/generate-app.ts b/packages/transform-imports/benchmark/scripts/generate-app.ts
new file mode 100644
index 0000000..59c8d51
--- /dev/null
+++ b/packages/transform-imports/benchmark/scripts/generate-app.ts
@@ -0,0 +1,194 @@
+/**
+ * App generator for transform-imports benchmark.
+ * Generates ~100 JS files with varied import patterns.
+ * Uses seeded random (seed=42) for deterministic generation.
+ */
+
+import { writeFileSync, mkdirSync, existsSync, rmSync } from 'node:fs'
+import { join } from 'node:path'
+import { SeededRandom } from '@rolldown/benchmark-utils/seeded-random'
+
+const rng = new SeededRandom(42)
+
+// Mock libraries whose imports will be transformed
+const LIBRARIES = ['ui-components', 'utils-lib', 'icons-pack', 'data-helpers', 'form-controls']
+
+// Members that can be imported from each library
+const MEMBERS: Record = {
+ 'ui-components': [
+ 'Button',
+ 'Card',
+ 'Modal',
+ 'Tooltip',
+ 'Dropdown',
+ 'Avatar',
+ 'Badge',
+ 'Breadcrumb',
+ 'Carousel',
+ 'Collapse',
+ 'DatePicker',
+ 'Divider',
+ 'Drawer',
+ 'Input',
+ 'Menu',
+ 'Pagination',
+ 'Popover',
+ 'Progress',
+ 'Radio',
+ 'Select',
+ 'Slider',
+ 'Switch',
+ 'Table',
+ 'Tabs',
+ 'Tag',
+ 'TimePicker',
+ 'Transfer',
+ 'TreeSelect',
+ 'Upload',
+ ],
+ 'utils-lib': [
+ 'debounce',
+ 'throttle',
+ 'cloneDeep',
+ 'isEmpty',
+ 'isEqual',
+ 'merge',
+ 'groupBy',
+ 'sortBy',
+ 'uniqBy',
+ 'flattenDeep',
+ 'mapValues',
+ 'pickBy',
+ 'omitBy',
+ 'capitalize',
+ 'camelCase',
+ 'kebabCase',
+ ],
+ 'icons-pack': [
+ 'ArrowUp',
+ 'ArrowDown',
+ 'ArrowLeft',
+ 'ArrowRight',
+ 'Check',
+ 'Close',
+ 'Edit',
+ 'Delete',
+ 'Search',
+ 'Filter',
+ 'Settings',
+ 'Home',
+ 'User',
+ 'Bell',
+ 'Star',
+ 'Heart',
+ 'Mail',
+ 'Lock',
+ 'Unlock',
+ 'Calendar',
+ ],
+ 'data-helpers': [
+ 'fetchData',
+ 'postData',
+ 'putData',
+ 'deleteData',
+ 'parseResponse',
+ 'formatError',
+ 'buildQuery',
+ 'encodeParams',
+ 'decodeParams',
+ 'validateSchema',
+ 'transformPayload',
+ 'serializeForm',
+ ],
+ 'form-controls': [
+ 'TextField',
+ 'NumberField',
+ 'CheckboxField',
+ 'RadioField',
+ 'SelectField',
+ 'DateField',
+ 'FileField',
+ 'TextAreaField',
+ 'PasswordField',
+ 'SearchField',
+ 'ColorField',
+ 'RangeField',
+ ],
+}
+
+type PatternType = 'named' | 'mixed' | 'reexport' | 'dynamic' | 'sideEffect'
+const PATTERN_TYPES: PatternType[] = [
+ 'named',
+ 'named',
+ 'named',
+ 'mixed',
+ 'reexport',
+ 'dynamic',
+ 'sideEffect',
+]
+
+function generateNamedImports(lib: string, members: string[]): string {
+ const count = rng.nextInt(4) + 1
+ const picked = rng.pickN(members, Math.min(count, members.length))
+ const usage = picked.map((m) => `console.log(${m})`).join('\n')
+ return `import { ${picked.join(', ')} } from '${lib}'\n${usage}\n`
+}
+
+function generateMixedImports(lib: string, members: string[]): string {
+ const picked = rng.pickN(members, Math.min(2, members.length))
+ const usage = picked.map((m) => `console.log(${m})`).join('\n')
+ return `import Lib, { ${picked.join(', ')} } from '${lib}'\nconsole.log(Lib)\n${usage}\n`
+}
+
+function generateReexport(lib: string, members: string[]): string {
+ const picked = rng.pickN(members, Math.min(3, members.length))
+ return `export { ${picked.join(', ')} } from '${lib}'\n`
+}
+
+function generateDynamicImport(lib: string): string {
+ return `const mod = await import('${lib}')\nconsole.log(mod)\n`
+}
+
+function generateSideEffectImport(lib: string): string {
+ return `import '${lib}'\n`
+}
+
+const GENERATORS: Record string> = {
+ named: generateNamedImports,
+ mixed: generateMixedImports,
+ reexport: generateReexport,
+ dynamic: generateDynamicImport,
+ sideEffect: generateSideEffectImport,
+}
+
+function main() {
+ const appDir = join(import.meta.dirname, '../shared-app/src')
+ if (existsSync(appDir)) rmSync(appDir, { recursive: true })
+ mkdirSync(join(appDir, 'modules'), { recursive: true })
+
+ const TOTAL = 100
+ const files: string[] = []
+
+ for (let i = 0; i < TOTAL; i++) {
+ const lib = rng.pick(LIBRARIES)
+ const members = MEMBERS[lib]
+ const patternType = rng.pick(PATTERN_TYPES)
+ const content = GENERATORS[patternType](lib, members)
+ const filename = `module${i + 1}.js`
+ writeFileSync(join(appDir, 'modules', filename), content)
+ files.push(filename)
+ }
+
+ // Generate entry file that imports all modules
+ const entry = files
+ .map((f, i) => `import './modules/${f}'\nconsole.log('module ${i + 1}')`)
+ .join('\n')
+ writeFileSync(join(appDir, 'index.js'), entry + '\n')
+
+ console.log(`Generated ${TOTAL} modules in ${appDir}/modules/`)
+ for (const type of PATTERN_TYPES) {
+ console.log(` Pattern distribution includes: ${type}`)
+ }
+}
+
+main()
diff --git a/packages/transform-imports/benchmark/vitest.config.ts b/packages/transform-imports/benchmark/vitest.config.ts
new file mode 100644
index 0000000..bd4ac39
--- /dev/null
+++ b/packages/transform-imports/benchmark/vitest.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ name: 'benchmark-transform-imports',
+ },
+})
diff --git a/packages/transform-imports/package.json b/packages/transform-imports/package.json
new file mode 100644
index 0000000..439a8db
--- /dev/null
+++ b/packages/transform-imports/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "@rolldown/plugin-transform-imports",
+ "version": "0.1.0",
+ "description": "Rolldown plugin for transforming import/exports to barrel files",
+ "keywords": [
+ "imports",
+ "modularize",
+ "plugin",
+ "rolldown",
+ "rolldown-plugin",
+ "transform-imports",
+ "tree-shaking"
+ ],
+ "homepage": "https://github.com/rolldown/plugins/tree/main/packages/transform-imports#readme",
+ "bugs": {
+ "url": "https://github.com/rolldown/plugins/issues"
+ },
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/rolldown/plugins.git",
+ "directory": "packages/transform-imports"
+ },
+ "files": [
+ "dist"
+ ],
+ "type": "module",
+ "exports": "./dist/index.mjs",
+ "scripts": {
+ "dev": "tsdown --watch",
+ "build": "tsdown",
+ "test": "vitest --project transform-imports",
+ "prepublishOnly": "pnpm run build"
+ },
+ "dependencies": {
+ "rolldown-string": "^0.3.0"
+ },
+ "devDependencies": {
+ "rolldown": "^1.0.0-rc.13",
+ "tinyglobby": "^0.2.15"
+ },
+ "peerDependencies": {
+ "rolldown": "^1.0.0-rc.13",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ },
+ "engines": {
+ "node": ">=22.12.0 || ^24.0.0"
+ },
+ "compatiblePackages": {
+ "schemaVersion": 1,
+ "rollup": {
+ "type": "incompatible",
+ "reason": "Uses Rolldown-specific APIs"
+ }
+ }
+}
diff --git a/packages/transform-imports/src/index.ts b/packages/transform-imports/src/index.ts
new file mode 100644
index 0000000..f5bb0bc
--- /dev/null
+++ b/packages/transform-imports/src/index.ts
@@ -0,0 +1,271 @@
+import { withMagicString, type RolldownString } from 'rolldown-string'
+import type { Plugin } from 'rolldown'
+import { Visitor } from 'rolldown/utils'
+import type { ESTree } from 'rolldown/utils'
+import type { TransformImportsOptions, TransformConfig, TransformPattern } from './types.js'
+import { processTemplate } from './template.js'
+
+export type {
+ TransformImportsOptions,
+ TransformConfig,
+ PluginConfig,
+ TransformPattern,
+} from './types.js'
+
+function getName(node: ESTree.ModuleExportName): string {
+ return node.type === 'Literal' ? node.value : node.name
+}
+
+interface CompiledModule {
+ regex: RegExp
+ config: TransformConfig
+}
+
+function compileModulePatterns(modules: Record): CompiledModule[] {
+ return Object.entries(modules).map(([pattern, config]) => ({
+ regex:
+ pattern.startsWith('^') && pattern.endsWith('$')
+ ? new RegExp(pattern)
+ : new RegExp(`^${pattern}$`),
+ config,
+ }))
+}
+
+function buildCodeFilter(modules: Record): RegExp | undefined {
+ const literals = Object.keys(modules)
+ .map((pattern) => {
+ // Extract literal prefix before first regex metacharacter
+ const match = pattern.match(/^[^\\?*+[\](){}|^$.]+/)
+ return match ? match[0] : ''
+ })
+ .filter(Boolean)
+ if (literals.length === 0) return undefined
+
+ const escaped = literals.map((lit) => lit.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
+ return new RegExp(escaped.join('|'))
+}
+
+function findMatchingModule(
+ source: string,
+ compiledModules: CompiledModule[],
+): { config: TransformConfig; matches: RegExpMatchArray } | null {
+ for (const { regex, config } of compiledModules) {
+ const match = source.match(regex)
+ if (match) return { config, matches: match }
+ }
+ return null
+}
+
+function resolveTransformPath(
+ transform: TransformPattern,
+ member: string,
+ moduleMatches: RegExpMatchArray,
+): string {
+ let result: string
+
+ if (typeof transform === 'string') {
+ result = processTemplate(transform, member, moduleMatches, null)
+ } else {
+ result = resolveFromTuples(transform, member, moduleMatches)
+ }
+
+ // Clean up consecutive slashes from empty template variables
+ return result.replace(/\/\/+/g, '/')
+}
+
+function resolveFromTuples(
+ tuples: [string, string][],
+ member: string,
+ moduleMatches: RegExpMatchArray,
+): string {
+ for (const [pattern, template] of tuples) {
+ if (pattern === '*') {
+ return processTemplate(template, member, moduleMatches, null)
+ }
+ // Treat all non-wildcard patterns as regex
+ const regex = new RegExp(`^${pattern}$`)
+ const match = member.match(regex)
+ if (match) {
+ return processTemplate(template, member, moduleMatches, match)
+ }
+ }
+
+ return member
+}
+
+function processImportDeclaration(
+ s: RolldownString,
+ decl: ESTree.ImportDeclaration,
+ compiledModules: CompiledModule[],
+): void {
+ const source = decl.source.value
+ const match = findMatchingModule(source, compiledModules)
+ if (!match) return
+
+ const { config, matches } = match
+ const specifiers = decl.specifiers
+
+ // Check preventFullImport
+ if (config.preventFullImport) {
+ if (specifiers.some((sp) => sp.type === 'ImportNamespaceSpecifier')) {
+ throw new Error(`preventFullImport: namespace import of "${source}" is not allowed`)
+ }
+ if (specifiers.length === 0) {
+ throw new Error(`preventFullImport: side-effect import of "${source}" is not allowed`)
+ }
+ }
+
+ // Side-effect import with no specifiers: rewrite source path
+ if (specifiers.length === 0) {
+ const transformedPath = resolveTransformPath(config.transform, '', matches)
+ s.update(decl.source.start, decl.source.end, `"${transformedPath}"`)
+ return
+ }
+
+ // Check if any specifiers will actually be transformed
+ const hasTransformed = specifiers.some(
+ (sp) =>
+ sp.type === 'ImportSpecifier' ||
+ (sp.type === 'ImportDefaultSpecifier' && config.handleDefaultImport) ||
+ (sp.type === 'ImportNamespaceSpecifier' && config.handleNamespaceImport),
+ )
+
+ // If nothing will be transformed, leave the import unchanged
+ if (!hasTransformed) return
+
+ const newImports: string[] = []
+ for (const spec of specifiers) {
+ if (spec.type === 'ImportSpecifier') {
+ const imported = getName(spec.imported)
+ const local = getName(spec.local)
+ const transformedPath = resolveTransformPath(config.transform, imported, matches)
+
+ if (config.skipDefaultConversion) {
+ newImports.push(`import { ${local} } from "${transformedPath}";`)
+ } else {
+ newImports.push(`import ${local} from "${transformedPath}";`)
+ }
+ } else if (spec.type === 'ImportDefaultSpecifier') {
+ if (config.handleDefaultImport) {
+ const local = getName(spec.local)
+ const transformedPath = resolveTransformPath(config.transform, local, matches)
+ newImports.push(`import ${local} from "${transformedPath}";`)
+ }
+ // Non-handled defaults in mixed imports are dropped
+ } else if (spec.type === 'ImportNamespaceSpecifier') {
+ if (config.handleNamespaceImport) {
+ const local = getName(spec.local)
+ const transformedPath = resolveTransformPath(config.transform, local, matches)
+ newImports.push(`import * as ${local} from "${transformedPath}";`)
+ }
+ // Non-handled namespaces in mixed imports are dropped
+ }
+ }
+ s.update(decl.start, decl.end, newImports.join('\n'))
+}
+
+function processExportNamedDeclaration(
+ s: RolldownString,
+ decl: ESTree.ExportNamedDeclaration,
+ compiledModules: CompiledModule[],
+): void {
+ if (!decl.source) return
+
+ const source = decl.source.value
+ const match = findMatchingModule(source, compiledModules)
+ if (!match) return
+
+ const { config, matches } = match
+
+ const reexports: string[] = []
+ for (const spec of decl.specifiers) {
+ const local = getName(spec.local)
+ const exported = getName(spec.exported)
+ const transformedPath = resolveTransformPath(config.transform, local, matches)
+ if (config.skipDefaultConversion) {
+ reexports.push(`export { ${exported} } from "${transformedPath}";`)
+ } else {
+ reexports.push(`export { default as ${exported} } from "${transformedPath}";`)
+ }
+ }
+ s.update(decl.start, decl.end, reexports.join('\n'))
+}
+
+function processExportAllDeclaration(
+ s: RolldownString,
+ decl: ESTree.ExportAllDeclaration,
+ compiledModules: CompiledModule[],
+): void {
+ const source = decl.source.value
+ const match = findMatchingModule(source, compiledModules)
+ if (!match) return
+
+ const { config, matches } = match
+ const transformedPath = resolveTransformPath(config.transform, '', matches)
+ s.update(decl.source.start, decl.source.end, `"${transformedPath}"`)
+}
+
+function getStaticImportExpressionSource(source: ESTree.Expression): string | null {
+ if (source.type === 'Literal' && typeof source.value === 'string') {
+ return source.value
+ }
+ if (source.type === 'TemplateLiteral' && source.expressions.length === 0) {
+ const [quasi] = source.quasis
+ if (quasi) return quasi.value.cooked ?? quasi.value.raw
+ return ''
+ }
+ return null
+}
+
+function processImportExpression(
+ s: RolldownString,
+ decl: ESTree.ImportExpression,
+ compiledModules: CompiledModule[],
+): void {
+ const source = getStaticImportExpressionSource(decl.source)
+ if (source == null) return
+
+ const match = findMatchingModule(source, compiledModules)
+ if (!match) return
+
+ const { config, matches } = match
+ const transformedPath = resolveTransformPath(config.transform, '', matches)
+ s.update(decl.source.start, decl.source.end, `"${transformedPath}"`)
+}
+
+export function transformImportsPlugin(options: TransformImportsOptions): Plugin {
+ const compiledModules = compileModulePatterns(options.modules)
+ const codeFilter = buildCodeFilter(options.modules)
+
+ return {
+ name: 'rolldown-plugin-transform-imports',
+
+ transform: {
+ filter: {
+ id: /\.[jt]sx?$/,
+ ...(codeFilter && { code: { include: codeFilter } }),
+ },
+
+ handler: withMagicString(function (this, s, _id, meta) {
+ const ast = meta?.ast ?? this.parse(s.original)
+
+ new Visitor({
+ ImportDeclaration(node: ESTree.ImportDeclaration) {
+ processImportDeclaration(s, node, compiledModules)
+ },
+ ExportNamedDeclaration(node: ESTree.ExportNamedDeclaration) {
+ processExportNamedDeclaration(s, node, compiledModules)
+ },
+ ExportAllDeclaration(node: ESTree.ExportAllDeclaration) {
+ processExportAllDeclaration(s, node, compiledModules)
+ },
+ ImportExpression(node: ESTree.ImportExpression) {
+ processImportExpression(s, node, compiledModules)
+ },
+ }).visit(ast)
+ }),
+ },
+ }
+}
+
+export default transformImportsPlugin
diff --git a/packages/transform-imports/src/template.ts b/packages/transform-imports/src/template.ts
new file mode 100644
index 0000000..fba14c3
--- /dev/null
+++ b/packages/transform-imports/src/template.ts
@@ -0,0 +1,79 @@
+function splitWords(str: string): string[] {
+ return str
+ .replace(/([a-z0-9])([A-Z])/g, '$1\0$2')
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1\0$2')
+ .split('\0')
+}
+
+function camelCase(str: string): string {
+ const words = splitWords(str)
+ return words
+ .map((w, i) =>
+ i === 0 ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase(),
+ )
+ .join('')
+}
+
+function kebabCase(str: string): string {
+ return splitWords(str)
+ .map((w) => w.toLowerCase())
+ .join('-')
+}
+
+function snakeCase(str: string): string {
+ return splitWords(str)
+ .map((w) => w.toLowerCase())
+ .join('_')
+}
+
+function upperCase(str: string): string {
+ return str.toUpperCase()
+}
+
+const caseTransforms: Record string> = {
+ camelCase,
+ kebabCase,
+ snakeCase,
+ upperCase,
+}
+
+function resolveVariable(
+ variable: string,
+ member: string,
+ matches: RegExpMatchArray | null,
+ memberMatches: RegExpMatchArray | null,
+): string {
+ if (variable === 'member') return member
+
+ const matchesResult = variable.match(/^matches\.\[(\d+)\]$/)
+ if (matchesResult) {
+ return matches?.[Number.parseInt(matchesResult[1])] ?? ''
+ }
+
+ const memberMatchesResult = variable.match(/^memberMatches\.\[(\d+)\]$/)
+ if (memberMatchesResult) {
+ return memberMatches?.[Number.parseInt(memberMatchesResult[1])] ?? ''
+ }
+
+ return variable
+}
+
+export function processTemplate(
+ template: string,
+ member: string,
+ matches: RegExpMatchArray | null,
+ memberMatches: RegExpMatchArray | null,
+): string {
+ return template.replace(/\{\{(.*?)\}\}/g, (_match, expr: string) => {
+ const parts = expr.trim().split(/\s+/)
+ if (parts.length === 2) {
+ const transform = caseTransforms[parts[0]]
+ const value = resolveVariable(parts[1], member, matches, memberMatches)
+ return transform ? transform(value) : value
+ }
+ if (parts.length === 1) {
+ return resolveVariable(parts[0], member, matches, memberMatches)
+ }
+ return _match
+ })
+}
diff --git a/packages/transform-imports/src/types.ts b/packages/transform-imports/src/types.ts
new file mode 100644
index 0000000..52a94cd
--- /dev/null
+++ b/packages/transform-imports/src/types.ts
@@ -0,0 +1,20 @@
+export type TransformPattern = string | [pattern: string, template: string][]
+
+export interface TransformConfig {
+ /** Template with {{member}} placeholder: "lodash/{{member}}" or array of [pattern, template] tuples */
+ transform: TransformPattern
+ /** Throw error on `import * as X` or `import 'mod'` */
+ preventFullImport?: boolean
+ /** Keep `import { X }` instead of `import X` */
+ skipDefaultConversion?: boolean
+ /** Transform default imports using local name as member (default: false) */
+ handleDefaultImport?: boolean
+ /** Transform namespace imports using local name as member (default: false) */
+ handleNamespaceImport?: boolean
+}
+
+export type PluginConfig = Record
+
+export interface TransformImportsOptions {
+ modules: PluginConfig
+}
diff --git a/packages/transform-imports/tests/fixtures/antd/config.json b/packages/transform-imports/tests/fixtures/antd/config.json
new file mode 100644
index 0000000..cb0a3ba
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/antd/config.json
@@ -0,0 +1,6 @@
+{
+ "antd": {
+ "transform": "antd/lib/{{ kebabCase member }}",
+ "style": "style"
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/antd/input.js b/packages/transform-imports/tests/fixtures/antd/input.js
new file mode 100644
index 0000000..d1dcaec
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/antd/input.js
@@ -0,0 +1 @@
+export { Button, Card, InputNumber } from 'antd';
diff --git a/packages/transform-imports/tests/fixtures/antd/output.js b/packages/transform-imports/tests/fixtures/antd/output.js
new file mode 100644
index 0000000..f1b7846
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/antd/output.js
@@ -0,0 +1,4 @@
+import Button from "antd/lib/button";
+import Card from "antd/lib/card";
+import InputNumber from "antd/lib/input-number";
+export { Button, Card, InputNumber };
diff --git a/packages/transform-imports/tests/fixtures/camel-case/config.json b/packages/transform-imports/tests/fixtures/camel-case/config.json
new file mode 100644
index 0000000..53523b9
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/camel-case/config.json
@@ -0,0 +1,6 @@
+{
+ "my-library-2": {
+ "transform": "my-library-2/{{ camelCase member }}",
+ "skipDefaultConversion": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/camel-case/input.js b/packages/transform-imports/tests/fixtures/camel-case/input.js
new file mode 100644
index 0000000..3acb6ff
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/camel-case/input.js
@@ -0,0 +1,4 @@
+// Test camelCase transformation: MyModule -> myModule
+import { MyModule, SomeWidget, AnotherComponent } from "my-library-2";
+
+console.log(MyModule, SomeWidget, AnotherComponent);
diff --git a/packages/transform-imports/tests/fixtures/camel-case/output.js b/packages/transform-imports/tests/fixtures/camel-case/output.js
new file mode 100644
index 0000000..53dbe20
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/camel-case/output.js
@@ -0,0 +1,6 @@
+import { MyModule } from "my-library-2/myModule";
+import { SomeWidget } from "my-library-2/someWidget";
+import { AnotherComponent } from "my-library-2/anotherComponent";
+//#region virtual:entry.ts
+console.log(MyModule, SomeWidget, AnotherComponent);
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/default/config.json b/packages/transform-imports/tests/fixtures/default/config.json
new file mode 100644
index 0000000..17ab549
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/default/config.json
@@ -0,0 +1,6 @@
+{
+ "my-(module-namespace|default|mixed-(named|star))": {
+ "transform": "transformed-{{matches.[1]}}",
+ "skipDefaultConversion": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/default/input.js b/packages/transform-imports/tests/fixtures/default/input.js
new file mode 100644
index 0000000..3a59662
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/default/input.js
@@ -0,0 +1,2 @@
+import defaultExport from "my-default";
+export { defaultExport };
diff --git a/packages/transform-imports/tests/fixtures/default/output.js b/packages/transform-imports/tests/fixtures/default/output.js
new file mode 100644
index 0000000..3a59662
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/default/output.js
@@ -0,0 +1,2 @@
+import defaultExport from "my-default";
+export { defaultExport };
diff --git a/packages/transform-imports/tests/fixtures/dynamic-imports/config.json b/packages/transform-imports/tests/fixtures/dynamic-imports/config.json
new file mode 100644
index 0000000..b1a9f9e
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/dynamic-imports/config.json
@@ -0,0 +1,5 @@
+{
+ "antd": {
+ "transform": "antd/es/{{member}}"
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/dynamic-imports/input.js b/packages/transform-imports/tests/fixtures/dynamic-imports/input.js
new file mode 100644
index 0000000..287a264
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/dynamic-imports/input.js
@@ -0,0 +1,11 @@
+await import ('antd');
+await import (`antd`);
+await import(`antd/${x}`);
+
+if (ok) {
+ await import ('antd');
+}
+
+foo(import ('antd'));
+await import ('antd', { with: { type: 'json' } });
+await import ('antd/es/button');
diff --git a/packages/transform-imports/tests/fixtures/dynamic-imports/output.js b/packages/transform-imports/tests/fixtures/dynamic-imports/output.js
new file mode 100644
index 0000000..8ebd028
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/dynamic-imports/output.js
@@ -0,0 +1,9 @@
+//#region virtual:entry.ts
+await import("antd/es/");
+await import("antd/es/");
+await import(`antd/${x}`);
+if (ok) await import("antd/es/");
+foo(import("antd/es/"));
+await import("antd/es/", { with: { type: "json" } });
+await import("antd/es/button");
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/handle-default-import/config.json b/packages/transform-imports/tests/fixtures/handle-default-import/config.json
new file mode 100644
index 0000000..b366064
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/handle-default-import/config.json
@@ -0,0 +1,6 @@
+{
+ "my-lib": {
+ "transform": "my-lib/{{member}}",
+ "handleDefaultImport": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/handle-default-import/input.js b/packages/transform-imports/tests/fixtures/handle-default-import/input.js
new file mode 100644
index 0000000..2b12eb0
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/handle-default-import/input.js
@@ -0,0 +1,2 @@
+import Button from "my-lib";
+console.log(Button);
diff --git a/packages/transform-imports/tests/fixtures/handle-default-import/output.js b/packages/transform-imports/tests/fixtures/handle-default-import/output.js
new file mode 100644
index 0000000..f0a1066
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/handle-default-import/output.js
@@ -0,0 +1,4 @@
+import Button from "my-lib/Button";
+//#region virtual:entry.ts
+console.log(Button);
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/handle-default-mixed/config.json b/packages/transform-imports/tests/fixtures/handle-default-mixed/config.json
new file mode 100644
index 0000000..b366064
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/handle-default-mixed/config.json
@@ -0,0 +1,6 @@
+{
+ "my-lib": {
+ "transform": "my-lib/{{member}}",
+ "handleDefaultImport": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/handle-default-mixed/input.js b/packages/transform-imports/tests/fixtures/handle-default-mixed/input.js
new file mode 100644
index 0000000..05bf6a9
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/handle-default-mixed/input.js
@@ -0,0 +1,2 @@
+import Default, { Named } from "my-lib";
+console.log(Default, Named);
diff --git a/packages/transform-imports/tests/fixtures/handle-default-mixed/output.js b/packages/transform-imports/tests/fixtures/handle-default-mixed/output.js
new file mode 100644
index 0000000..b54ec65
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/handle-default-mixed/output.js
@@ -0,0 +1,5 @@
+import Default from "my-lib/Default";
+import Named from "my-lib/Named";
+//#region virtual:entry.ts
+console.log(Default, Named);
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/handle-namespace-import/config.json b/packages/transform-imports/tests/fixtures/handle-namespace-import/config.json
new file mode 100644
index 0000000..71be839
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/handle-namespace-import/config.json
@@ -0,0 +1,6 @@
+{
+ "my-lib": {
+ "transform": "my-lib/{{member}}",
+ "handleNamespaceImport": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/handle-namespace-import/input.js b/packages/transform-imports/tests/fixtures/handle-namespace-import/input.js
new file mode 100644
index 0000000..b3ba4f6
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/handle-namespace-import/input.js
@@ -0,0 +1,2 @@
+import * as Button from "my-lib";
+console.log(Button);
diff --git a/packages/transform-imports/tests/fixtures/handle-namespace-import/output.js b/packages/transform-imports/tests/fixtures/handle-namespace-import/output.js
new file mode 100644
index 0000000..234c52f
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/handle-namespace-import/output.js
@@ -0,0 +1,4 @@
+import * as Button from "my-lib/Button";
+//#region virtual:entry.ts
+console.log(Button);
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/kebab-case/config.json b/packages/transform-imports/tests/fixtures/kebab-case/config.json
new file mode 100644
index 0000000..b4405b9
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/kebab-case/config.json
@@ -0,0 +1,6 @@
+{
+ "my-library-3": {
+ "transform": "my-library-3/{{ kebabCase member }}",
+ "skipDefaultConversion": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/kebab-case/input.js b/packages/transform-imports/tests/fixtures/kebab-case/input.js
new file mode 100644
index 0000000..197c303
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/kebab-case/input.js
@@ -0,0 +1,4 @@
+// Test kebabCase transformation: MyModule -> my-module
+import { AnotherModule, MyFancyComponent, SomeWidget } from "my-library-3";
+
+console.log(AnotherModule, MyFancyComponent, SomeWidget);
diff --git a/packages/transform-imports/tests/fixtures/kebab-case/output.js b/packages/transform-imports/tests/fixtures/kebab-case/output.js
new file mode 100644
index 0000000..4c42cb3
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/kebab-case/output.js
@@ -0,0 +1,6 @@
+import { AnotherModule } from "my-library-3/another-module";
+import { MyFancyComponent } from "my-library-3/my-fancy-component";
+import { SomeWidget } from "my-library-3/some-widget";
+//#region virtual:entry.ts
+console.log(AnotherModule, MyFancyComponent, SomeWidget);
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/mapping-export/config.json b/packages/transform-imports/tests/fixtures/mapping-export/config.json
new file mode 100644
index 0000000..8e1dc69
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/mapping-export/config.json
@@ -0,0 +1,12 @@
+{
+ "my-library-4": {
+ "transform": [
+ ["foo", "my-library-4/this_is_foo"],
+ ["bar", "my-library-4/bar"],
+ ["use(\\w*)", "my-library-4/{{ kebabCase member }}/{{ kebabCase memberMatches.[1] }}"],
+ ["(\\w*)Icon", "my-library-4/{{ kebabCase memberMatches.[1] }}"],
+ ["*", "my-library-4/{{ upperCase member }}"]
+ ],
+ "skipDefaultConversion": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/mapping-export/input.js b/packages/transform-imports/tests/fixtures/mapping-export/input.js
new file mode 100644
index 0000000..ed144fa
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/mapping-export/input.js
@@ -0,0 +1,4 @@
+export { foo, bar } from "my-library-4";
+export { otherImport } from "my-library-4";
+export { useSomeAPI } from "my-library-4";
+export { ArrowUpIcon } from "my-library-4";
diff --git a/packages/transform-imports/tests/fixtures/mapping-export/output.js b/packages/transform-imports/tests/fixtures/mapping-export/output.js
new file mode 100644
index 0000000..6a3f51b
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/mapping-export/output.js
@@ -0,0 +1,6 @@
+import { foo } from "my-library-4/this_is_foo";
+import { bar } from "my-library-4/bar";
+import { otherImport } from "my-library-4/OTHERIMPORT";
+import { useSomeAPI } from "my-library-4/use-some-api/some-api";
+import { ArrowUpIcon } from "my-library-4/arrow-up";
+export { ArrowUpIcon, bar, foo, otherImport, useSomeAPI };
diff --git a/packages/transform-imports/tests/fixtures/mapping/config.json b/packages/transform-imports/tests/fixtures/mapping/config.json
new file mode 100644
index 0000000..8e1dc69
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/mapping/config.json
@@ -0,0 +1,12 @@
+{
+ "my-library-4": {
+ "transform": [
+ ["foo", "my-library-4/this_is_foo"],
+ ["bar", "my-library-4/bar"],
+ ["use(\\w*)", "my-library-4/{{ kebabCase member }}/{{ kebabCase memberMatches.[1] }}"],
+ ["(\\w*)Icon", "my-library-4/{{ kebabCase memberMatches.[1] }}"],
+ ["*", "my-library-4/{{ upperCase member }}"]
+ ],
+ "skipDefaultConversion": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/mapping/input.js b/packages/transform-imports/tests/fixtures/mapping/input.js
new file mode 100644
index 0000000..f9860cf
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/mapping/input.js
@@ -0,0 +1,6 @@
+import { foo, bar } from "my-library-4";
+import { otherImport } from "my-library-4";
+import { useSomeAPI } from "my-library-4";
+import { ArrowUpIcon } from "my-library-4";
+
+console.log(foo, bar, otherImport, useSomeAPI, ArrowUpIcon);
diff --git a/packages/transform-imports/tests/fixtures/mapping/output.js b/packages/transform-imports/tests/fixtures/mapping/output.js
new file mode 100644
index 0000000..f416085
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/mapping/output.js
@@ -0,0 +1,8 @@
+import { foo } from "my-library-4/this_is_foo";
+import { bar } from "my-library-4/bar";
+import { otherImport } from "my-library-4/OTHERIMPORT";
+import { useSomeAPI } from "my-library-4/use-some-api/some-api";
+import { ArrowUpIcon } from "my-library-4/arrow-up";
+//#region virtual:entry.ts
+console.log(foo, bar, otherImport, useSomeAPI, ArrowUpIcon);
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/member-import/config.json b/packages/transform-imports/tests/fixtures/member-import/config.json
new file mode 100644
index 0000000..c983152
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/member-import/config.json
@@ -0,0 +1,5 @@
+{
+ "react-bootstrap": {
+ "transform": "react-bootstrap/lib/{{member}}"
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/member-import/input.js b/packages/transform-imports/tests/fixtures/member-import/input.js
new file mode 100644
index 0000000..ac572cc
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/member-import/input.js
@@ -0,0 +1,3 @@
+import { Grid, Row as row } from 'react-bootstrap';
+
+console.log(Grid, row);
diff --git a/packages/transform-imports/tests/fixtures/member-import/output.js b/packages/transform-imports/tests/fixtures/member-import/output.js
new file mode 100644
index 0000000..d12bb22
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/member-import/output.js
@@ -0,0 +1,5 @@
+import Grid from "react-bootstrap/lib/Grid";
+import row from "react-bootstrap/lib/Row";
+//#region virtual:entry.ts
+console.log(Grid, row);
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/mixed-default/config.json b/packages/transform-imports/tests/fixtures/mixed-default/config.json
new file mode 100644
index 0000000..17ab549
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/mixed-default/config.json
@@ -0,0 +1,6 @@
+{
+ "my-(module-namespace|default|mixed-(named|star))": {
+ "transform": "transformed-{{matches.[1]}}",
+ "skipDefaultConversion": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/mixed-default/input.js b/packages/transform-imports/tests/fixtures/mixed-default/input.js
new file mode 100644
index 0000000..f9118cb
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/mixed-default/input.js
@@ -0,0 +1,3 @@
+import defaultExport, { someNamedExport } from "my-mixed-named";
+import otherDefaultExport, * as starExport from "my-mixed-star";
+console.log(defaultExport, someNamedExport, otherDefaultExport, starExport);
diff --git a/packages/transform-imports/tests/fixtures/mixed-default/output.js b/packages/transform-imports/tests/fixtures/mixed-default/output.js
new file mode 100644
index 0000000..8815218
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/mixed-default/output.js
@@ -0,0 +1,6 @@
+import { someNamedExport } from "transformed-mixed-named";
+import * as starExport from "my-mixed-star";
+import otherDefaultExport from "my-mixed-star";
+//#region virtual:entry.ts
+console.log(defaultExport, someNamedExport, otherDefaultExport, starExport);
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/namespace-export/config.json b/packages/transform-imports/tests/fixtures/namespace-export/config.json
new file mode 100644
index 0000000..2f9bba3
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/namespace-export/config.json
@@ -0,0 +1,7 @@
+{
+ "my-(module-namespace|default|mixed-(named|star))": {
+ "transform": "transformed-{{matches.[1]}}",
+ "skipDefaultConversion": true,
+ "handleNamespaceImport": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/namespace-export/input.js b/packages/transform-imports/tests/fixtures/namespace-export/input.js
new file mode 100644
index 0000000..f18be5c
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/namespace-export/input.js
@@ -0,0 +1 @@
+export * as someModule from "my-module-namespace";
diff --git a/packages/transform-imports/tests/fixtures/namespace-export/output.js b/packages/transform-imports/tests/fixtures/namespace-export/output.js
new file mode 100644
index 0000000..0958cbd
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/namespace-export/output.js
@@ -0,0 +1,2 @@
+import * as someModule from "transformed-module-namespace";
+export { someModule };
diff --git a/packages/transform-imports/tests/fixtures/namespace/config.json b/packages/transform-imports/tests/fixtures/namespace/config.json
new file mode 100644
index 0000000..17ab549
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/namespace/config.json
@@ -0,0 +1,6 @@
+{
+ "my-(module-namespace|default|mixed-(named|star))": {
+ "transform": "transformed-{{matches.[1]}}",
+ "skipDefaultConversion": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/namespace/input.js b/packages/transform-imports/tests/fixtures/namespace/input.js
new file mode 100644
index 0000000..a73487f
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/namespace/input.js
@@ -0,0 +1,2 @@
+import * as someModule from "my-module-namespace";
+export { someModule };
diff --git a/packages/transform-imports/tests/fixtures/namespace/output.js b/packages/transform-imports/tests/fixtures/namespace/output.js
new file mode 100644
index 0000000..a73487f
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/namespace/output.js
@@ -0,0 +1,2 @@
+import * as someModule from "my-module-namespace";
+export { someModule };
diff --git a/packages/transform-imports/tests/fixtures/pascal-case/config.json b/packages/transform-imports/tests/fixtures/pascal-case/config.json
new file mode 100644
index 0000000..5b6754b
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/pascal-case/config.json
@@ -0,0 +1,6 @@
+{
+ "test-pascal-case": {
+ "transform": "test-pascal-case/{{ member }}",
+ "skipDefaultConversion": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/pascal-case/input.js b/packages/transform-imports/tests/fixtures/pascal-case/input.js
new file mode 100644
index 0000000..079a937
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/pascal-case/input.js
@@ -0,0 +1,4 @@
+// Test that member name is preserved in path when using member template
+import { myComponent, anotherWidget } from "test-pascal-case";
+
+console.log(myComponent, anotherWidget);
diff --git a/packages/transform-imports/tests/fixtures/pascal-case/output.js b/packages/transform-imports/tests/fixtures/pascal-case/output.js
new file mode 100644
index 0000000..9b786a5
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/pascal-case/output.js
@@ -0,0 +1,5 @@
+import { myComponent } from "test-pascal-case/myComponent";
+import { anotherWidget } from "test-pascal-case/anotherWidget";
+//#region virtual:entry.ts
+console.log(myComponent, anotherWidget);
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/regex-export/config.json b/packages/transform-imports/tests/fixtures/regex-export/config.json
new file mode 100644
index 0000000..bfe8cee
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/regex-export/config.json
@@ -0,0 +1,5 @@
+{
+ "my-library/?(((\\w*)?/?)*)": {
+ "transform": "my-library/{{ matches.[1] }}/{{member}}"
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/regex-export/input.js b/packages/transform-imports/tests/fixtures/regex-export/input.js
new file mode 100644
index 0000000..992d999
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/regex-export/input.js
@@ -0,0 +1,3 @@
+export { MyModule } from "my-library";
+export { App } from "my-library/components";
+export { Header, Footer } from "my-library/components/App";
diff --git a/packages/transform-imports/tests/fixtures/regex-export/output.js b/packages/transform-imports/tests/fixtures/regex-export/output.js
new file mode 100644
index 0000000..eb511d8
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/regex-export/output.js
@@ -0,0 +1,5 @@
+import MyModule from "my-library/MyModule";
+import App from "my-library/components/App";
+import Header from "my-library/components/App/Header";
+import Footer from "my-library/components/App/Footer";
+export { App, Footer, Header, MyModule };
diff --git a/packages/transform-imports/tests/fixtures/regex/config.json b/packages/transform-imports/tests/fixtures/regex/config.json
new file mode 100644
index 0000000..bfe8cee
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/regex/config.json
@@ -0,0 +1,5 @@
+{
+ "my-library/?(((\\w*)?/?)*)": {
+ "transform": "my-library/{{ matches.[1] }}/{{member}}"
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/regex/input.js b/packages/transform-imports/tests/fixtures/regex/input.js
new file mode 100644
index 0000000..90bc366
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/regex/input.js
@@ -0,0 +1,5 @@
+import { MyModule } from "my-library";
+import { App } from "my-library/components";
+import { Header, Footer } from "my-library/components/App";
+
+console.log(MyModule, App, Header, Footer);
diff --git a/packages/transform-imports/tests/fixtures/regex/output.js b/packages/transform-imports/tests/fixtures/regex/output.js
new file mode 100644
index 0000000..582b911
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/regex/output.js
@@ -0,0 +1,7 @@
+import MyModule from "my-library/MyModule";
+import App from "my-library/components/App";
+import Header from "my-library/components/App/Header";
+import Footer from "my-library/components/App/Footer";
+//#region virtual:entry.ts
+console.log(MyModule, App, Header, Footer);
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/side-effect-imports/config.json b/packages/transform-imports/tests/fixtures/side-effect-imports/config.json
new file mode 100644
index 0000000..2c63c08
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/side-effect-imports/config.json
@@ -0,0 +1,2 @@
+{
+}
diff --git a/packages/transform-imports/tests/fixtures/side-effect-imports/input.js b/packages/transform-imports/tests/fixtures/side-effect-imports/input.js
new file mode 100644
index 0000000..35730f7
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/side-effect-imports/input.js
@@ -0,0 +1,2 @@
+import './upload/upload.ts';
+import './scripts/scripts.model.js';
\ No newline at end of file
diff --git a/packages/transform-imports/tests/fixtures/side-effect-imports/output.js b/packages/transform-imports/tests/fixtures/side-effect-imports/output.js
new file mode 100644
index 0000000..ee12862
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/side-effect-imports/output.js
@@ -0,0 +1,2 @@
+import "./upload/upload.ts";
+import "./scripts/scripts.model.js";
diff --git a/packages/transform-imports/tests/fixtures/simple-export/config.json b/packages/transform-imports/tests/fixtures/simple-export/config.json
new file mode 100644
index 0000000..b1128bf
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/simple-export/config.json
@@ -0,0 +1,13 @@
+{
+ "react-bootstrap": {
+ "transform": "react-bootstrap/lib/{{member}}"
+ },
+ "my-library-2": {
+ "transform": "my-library-2/{{ camelCase member }}",
+ "skipDefaultConversion": true
+ },
+ "my-library-3": {
+ "transform": "my-library-3/{{ kebabCase member }}",
+ "skipDefaultConversion": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/simple-export/input.js b/packages/transform-imports/tests/fixtures/simple-export/input.js
new file mode 100644
index 0000000..278480b
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/simple-export/input.js
@@ -0,0 +1,3 @@
+export { Grid, Row, Col as Col1 } from "react-bootstrap";
+export { MyModule, Widget } from "my-library-2";
+export { AnotherModule } from "my-library-3";
diff --git a/packages/transform-imports/tests/fixtures/simple-export/output.js b/packages/transform-imports/tests/fixtures/simple-export/output.js
new file mode 100644
index 0000000..a88346f
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/simple-export/output.js
@@ -0,0 +1,7 @@
+import Grid from "react-bootstrap/lib/Grid";
+import Row from "react-bootstrap/lib/Row";
+import Col1 from "react-bootstrap/lib/Col";
+import { MyModule } from "my-library-2/myModule";
+import { Widget } from "my-library-2/widget";
+import { AnotherModule } from "my-library-3/another-module";
+export { AnotherModule, Col1, Grid, MyModule, Row, Widget };
diff --git a/packages/transform-imports/tests/fixtures/simple/config.json b/packages/transform-imports/tests/fixtures/simple/config.json
new file mode 100644
index 0000000..b1128bf
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/simple/config.json
@@ -0,0 +1,13 @@
+{
+ "react-bootstrap": {
+ "transform": "react-bootstrap/lib/{{member}}"
+ },
+ "my-library-2": {
+ "transform": "my-library-2/{{ camelCase member }}",
+ "skipDefaultConversion": true
+ },
+ "my-library-3": {
+ "transform": "my-library-3/{{ kebabCase member }}",
+ "skipDefaultConversion": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/simple/input.js b/packages/transform-imports/tests/fixtures/simple/input.js
new file mode 100644
index 0000000..f4ed62c
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/simple/input.js
@@ -0,0 +1,5 @@
+import { Grid, Row, Col as Col1 } from "react-bootstrap";
+import { MyModule, Widget } from "my-library-2";
+import { AnotherModule } from "my-library-3";
+
+console.log(Grid, Row, Col1, MyModule, Widget, AnotherModule);
diff --git a/packages/transform-imports/tests/fixtures/simple/output.js b/packages/transform-imports/tests/fixtures/simple/output.js
new file mode 100644
index 0000000..75960af
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/simple/output.js
@@ -0,0 +1,9 @@
+import Grid from "react-bootstrap/lib/Grid";
+import Row from "react-bootstrap/lib/Row";
+import Col1 from "react-bootstrap/lib/Col";
+import { MyModule } from "my-library-2/myModule";
+import { Widget } from "my-library-2/widget";
+import { AnotherModule } from "my-library-3/another-module";
+//#region virtual:entry.ts
+console.log(Grid, Row, Col1, MyModule, Widget, AnotherModule);
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/skip-default-conversion/config.json b/packages/transform-imports/tests/fixtures/skip-default-conversion/config.json
new file mode 100644
index 0000000..53523b9
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/skip-default-conversion/config.json
@@ -0,0 +1,6 @@
+{
+ "my-library-2": {
+ "transform": "my-library-2/{{ camelCase member }}",
+ "skipDefaultConversion": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/skip-default-conversion/input.js b/packages/transform-imports/tests/fixtures/skip-default-conversion/input.js
new file mode 100644
index 0000000..35433f6
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/skip-default-conversion/input.js
@@ -0,0 +1,4 @@
+// Test skipDefaultConversion: keeps named imports instead of converting to default
+import { MyModule, Widget } from "my-library-2";
+
+console.log(MyModule, Widget);
diff --git a/packages/transform-imports/tests/fixtures/skip-default-conversion/output.js b/packages/transform-imports/tests/fixtures/skip-default-conversion/output.js
new file mode 100644
index 0000000..a790773
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/skip-default-conversion/output.js
@@ -0,0 +1,5 @@
+import { MyModule } from "my-library-2/myModule";
+import { Widget } from "my-library-2/widget";
+//#region virtual:entry.ts
+console.log(MyModule, Widget);
+//#endregion
diff --git a/packages/transform-imports/tests/fixtures/snake-case/config.json b/packages/transform-imports/tests/fixtures/snake-case/config.json
new file mode 100644
index 0000000..5044d92
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/snake-case/config.json
@@ -0,0 +1,6 @@
+{
+ "test-snake-case": {
+ "transform": "test-snake-case/{{ snakeCase member }}",
+ "skipDefaultConversion": true
+ }
+}
diff --git a/packages/transform-imports/tests/fixtures/snake-case/input.js b/packages/transform-imports/tests/fixtures/snake-case/input.js
new file mode 100644
index 0000000..3208644
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/snake-case/input.js
@@ -0,0 +1,4 @@
+// Test snakeCase transformation
+import { MyModule, SomeWidget } from "test-snake-case";
+
+console.log(MyModule, SomeWidget);
diff --git a/packages/transform-imports/tests/fixtures/snake-case/output.js b/packages/transform-imports/tests/fixtures/snake-case/output.js
new file mode 100644
index 0000000..1d93460
--- /dev/null
+++ b/packages/transform-imports/tests/fixtures/snake-case/output.js
@@ -0,0 +1,5 @@
+import { MyModule } from "test-snake-case/my_module";
+import { SomeWidget } from "test-snake-case/some_widget";
+//#region virtual:entry.ts
+console.log(MyModule, SomeWidget);
+//#endregion
diff --git a/packages/transform-imports/tests/transform.test.ts b/packages/transform-imports/tests/transform.test.ts
new file mode 100644
index 0000000..4388b2c
--- /dev/null
+++ b/packages/transform-imports/tests/transform.test.ts
@@ -0,0 +1,80 @@
+import { describe, it, expect } from 'vitest'
+import { rolldown } from 'rolldown'
+import { transformImportsPlugin, type PluginConfig } from '../src/index.ts'
+import { globSync } from 'tinyglobby'
+import { readFileSync } from 'node:fs'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+describe('transform-imports', () => {
+ it('throws on namespace import when preventFullImport is true', async () => {
+ const input = `import * as Bootstrap from 'react-bootstrap'\nconsole.log(Bootstrap)`
+ const config = {
+ 'react-bootstrap': {
+ transform: 'react-bootstrap/lib/{{member}}',
+ preventFullImport: true,
+ },
+ }
+
+ await expect(transform(input, config)).rejects.toThrow('preventFullImport')
+ })
+
+ it('throws on side-effect import when preventFullImport is true', async () => {
+ const input = `import 'react-bootstrap'`
+ const config = {
+ 'react-bootstrap': {
+ transform: 'react-bootstrap/lib/{{member}}',
+ preventFullImport: true,
+ },
+ }
+
+ await expect(transform(input, config)).rejects.toThrow('preventFullImport')
+ })
+})
+
+const fixturesDir = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
+const fixturePaths = globSync('*/input.js', { cwd: fixturesDir })
+
+describe('fixtures', () => {
+ for (const inputPath of fixturePaths) {
+ const fixtureName = dirname(inputPath)
+ const input = readFileSync(join(fixturesDir, inputPath), 'utf-8')
+ const configPath = join(fixturesDir, fixtureName, 'config.json')
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'))
+
+ it(fixtureName, async () => {
+ const result = await transform(input, config)
+ await expect(result).toMatchFileSnapshot(join(fixturesDir, fixtureName, 'output.js'))
+ })
+ }
+})
+
+async function transform(code: string, modules: PluginConfig): Promise {
+ const build = await rolldown({
+ input: 'virtual:entry.ts',
+ plugins: [
+ {
+ name: 'virtual',
+ resolveId(id) {
+ if (id === 'virtual:entry.ts') return id
+ // Mark transformed imports as external
+ return { id, external: true }
+ },
+ load(id) {
+ if (id === 'virtual:entry.ts') return code
+ },
+ },
+ transformImportsPlugin({ modules }),
+ ],
+ })
+
+ const { output } = await build.generate({ format: 'esm' })
+ return stripRolldownRuntime(output[0].code)
+}
+
+function stripRolldownRuntime(code: string): string {
+ return code.replace(
+ /\/\/#region \\0rolldown\/runtime\.js[\s\S]*?\/\/#endregion\n*/g,
+ '// [rolldown runtime elided]\n',
+ )
+}
diff --git a/packages/transform-imports/tsdown.config.ts b/packages/transform-imports/tsdown.config.ts
new file mode 100644
index 0000000..a3a2720
--- /dev/null
+++ b/packages/transform-imports/tsdown.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from 'tsdown'
+
+export default defineConfig({
+ entry: './src/index.ts',
+ dts: {
+ tsconfig: '../../tsconfig.common.json',
+ tsgo: true,
+ },
+})
diff --git a/packages/transform-imports/vitest.config.ts b/packages/transform-imports/vitest.config.ts
new file mode 100644
index 0000000..e2fabd7
--- /dev/null
+++ b/packages/transform-imports/vitest.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ name: 'transform-imports',
+ },
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 29afc40..e08c946 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -125,6 +125,15 @@ importers:
specifier: ^8.0.7
version: 8.0.7(@types/node@24.12.2)(esbuild@0.27.3)
+ examples/transform-imports:
+ devDependencies:
+ '@rolldown/plugin-transform-imports':
+ specifier: workspace:*
+ version: link:../../packages/transform-imports
+ vite:
+ specifier: ^8.0.7
+ version: 8.0.7(@types/node@24.12.2)(esbuild@0.27.3)
+
internal-packages/benchmark-utils:
devDependencies:
'@oxc-node/core':
@@ -151,6 +160,9 @@ importers:
'@swc/plugin-styled-jsx':
specifier: ^13.8.0
version: 13.8.0
+ '@swc/plugin-transform-imports':
+ specifier: ^12.8.0
+ version: 12.8.0
rolldown:
specifier: ^1.0.0-rc.13
version: 1.0.0-rc.13
@@ -423,6 +435,55 @@ importers:
specifier: ^1.0.0-rc.13
version: 1.0.0-rc.13
+ packages/transform-imports:
+ dependencies:
+ rolldown-string:
+ specifier: ^0.3.0
+ version: 0.3.0(rolldown@1.0.0-rc.13)
+ vite:
+ specifier: ^8.0.0
+ version: 8.0.7(@types/node@24.12.2)(esbuild@0.27.3)
+ devDependencies:
+ rolldown:
+ specifier: ^1.0.0-rc.13
+ version: 1.0.0-rc.13
+ tinyglobby:
+ specifier: ^0.2.15
+ version: 0.2.15
+
+ packages/transform-imports/benchmark:
+ devDependencies:
+ '@oxc-node/cli':
+ specifier: ^0.1.0
+ version: 0.1.0
+ '@rolldown/benchmark-utils':
+ specifier: workspace:*
+ version: link:../../../internal-packages/benchmark-utils
+ '@rolldown/plugin-babel':
+ specifier: file:../../babel
+ version: link:../../babel
+ '@rolldown/plugin-transform-imports':
+ specifier: workspace:*
+ version: link:..
+ '@rollup/plugin-swc':
+ specifier: ^0.4.0
+ version: 0.4.0(@swc/core@1.15.24)
+ '@swc/core':
+ specifier: ^1.15.24
+ version: 1.15.24
+ '@swc/plugin-transform-imports':
+ specifier: ^12.8.0
+ version: 12.8.0
+ '@types/node':
+ specifier: ^24.12.2
+ version: 24.12.2
+ babel-plugin-transform-imports:
+ specifier: ^2.0.0
+ version: 2.0.0
+ rolldown:
+ specifier: ^1.0.0-rc.13
+ version: 1.0.0-rc.13
+
scripts:
devDependencies:
'@vitejs/release-scripts':
@@ -1766,6 +1827,9 @@ packages:
'@swc/plugin-styled-jsx@13.8.0':
resolution: {integrity: sha512-ptbmkoEgdiF25SOvQyR0icd5ARj7kuaJmQAyDZl/xPfGIKt9b7LoXef1ZoY1i7osXCJ/m9cUkR5Lrf57E8cpUA==}
+ '@swc/plugin-transform-imports@12.8.0':
+ resolution: {integrity: sha512-5/iZPNxrQlBpKRWLmLjk+YUBXJ5uob5CYUeWEV5Rre/HqPNtsdKS7lv8cz+0b6xFZ9iggxQ+GOAmAReYxal73A==}
+
'@swc/types@0.1.26':
resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==}
@@ -1921,6 +1985,9 @@ packages:
babel-plugin-react-remove-properties@0.3.1:
resolution: {integrity: sha512-JQ0oEgC4P6TZxSqEaoPZdbgE2L38Kz7RgTgGcOUUR24Z19OiGhnhdXkXkwwsYyx6LOr/bMY5ljDADhxDr/JQug==}
+ babel-plugin-transform-imports@2.0.0:
+ resolution: {integrity: sha512-65ewumYJ85QiXdcB/jmiU0y0jg6eL6CdnDqQAqQ8JMOKh1E52VPG3NJzbVKWcgovUR5GBH8IWpCXQ7I8Q3wjgw==}
+
baseline-browser-mapping@2.10.0:
resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==}
engines: {node: '>=6.0.0'}
@@ -2162,6 +2229,18 @@ packages:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'}
+ is-extglob@1.0.0:
+ resolution: {integrity: sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@2.0.1:
+ resolution: {integrity: sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==}
+ engines: {node: '>=0.10.0'}
+
+ is-invalid-path@0.1.0:
+ resolution: {integrity: sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==}
+ engines: {node: '>=0.10.0'}
+
is-obj@2.0.0:
resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
engines: {node: '>=8'}
@@ -2182,6 +2261,10 @@ packages:
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
engines: {node: '>=18'}
+ is-valid-path@0.1.1:
+ resolution: {integrity: sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==}
+ engines: {node: '>=0.10.0'}
+
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -3817,6 +3900,10 @@ snapshots:
dependencies:
'@swc/counter': 0.1.3
+ '@swc/plugin-transform-imports@12.8.0':
+ dependencies:
+ '@swc/counter': 0.1.3
+
'@swc/types@0.1.26':
dependencies:
'@swc/counter': 0.1.3
@@ -3975,6 +4062,11 @@ snapshots:
babel-plugin-react-remove-properties@0.3.1: {}
+ babel-plugin-transform-imports@2.0.0:
+ dependencies:
+ '@babel/types': 7.29.0
+ is-valid-path: 0.1.1
+
baseline-browser-mapping@2.10.0: {}
birpc@4.0.0: {}
@@ -4223,6 +4315,16 @@ snapshots:
dependencies:
hasown: 2.0.2
+ is-extglob@1.0.0: {}
+
+ is-glob@2.0.1:
+ dependencies:
+ is-extglob: 1.0.0
+
+ is-invalid-path@0.1.0:
+ dependencies:
+ is-glob: 2.0.1
+
is-obj@2.0.0: {}
is-plain-obj@4.1.0: {}
@@ -4233,6 +4335,10 @@ snapshots:
is-unicode-supported@2.1.0: {}
+ is-valid-path@0.1.1:
+ dependencies:
+ is-invalid-path: 0.1.0
+
isexe@2.0.0: {}
js-tokens@10.0.0: {}
diff --git a/scripts/release.ts b/scripts/release.ts
index 0f85ee9..e58a74c 100644
--- a/scripts/release.ts
+++ b/scripts/release.ts
@@ -12,6 +12,7 @@ await release({
'plugin-emotion',
'plugin-jsx-remove-attributes',
'plugin-styled-jsx',
+ 'plugin-transform-imports',
'oxc-unshadowed-visitor',
],
toTag: (pkg, version) => `${pkg}@${version}`,