unjs / unimport

Unified utils for auto importing APIs in modules.
MIT License
528 stars 61 forks source link

Import groups / namespaces #317

Open brawaru opened 9 months ago

brawaru commented 9 months ago

Describe the feature

Automatic imports are nice, but sometimes you don't want to clutter your virtual global scope with a lot of different things that share something in common. It would be great if Unimport could help you in situations like this.

Imagine a situation where you have a folder messages where each file would contain its own localisable message declarations exported as defaults:

// messages/page-titles.ts
export default defineMessages({
  projects: {
    id: 'page.projects.title',
    defaultMessage: 'Projects',
  },
})
// messages/errors.ts
export default defineMessages({
  notFound: {
    id: 'error.not-found',
    defaultMessage: 'Resource not found',
  },
})
// messages/utils.ts
export function getWorldGreeting() {
  return defineMessage({
    id: 'greetings.world',
    defaultMessage: 'Hello, world!',
  })
}

You don't really want to import each file separately, since pageTitles and errors are quite confusing. Nor do you want to rely on some naming conventions (like having files named like errors-messages to keep it clean.

In this case you'd likely prefer to have a globalMessages namespace, that would've contained all the imports than Unimport discovered, like pageTitles, errors, and anything else. It would also tree-shaking, as if you've done a specific import rather than imported everything at once.

To do so manually without Unimport right now you can make a barrel file:

// messages/index.barrel.ts
import pageTitles from "./page-titles.ts"
import errors from "./errors.ts"
export * from "./utils.ts"
export { pageTitles, errors }

And then the file that would export that barrel:

// messages/index.ts
export * as globalMessages from "./index.barrel.ts";

Now you can import { globalMessages } from '~/messages'. You can also add this to your Unimport.

But what if Unimport could group imports like this automatically for you?

You'd add a scan option:

unimport.vite({
  dirs: [
    './utils/*',
    { from: './messages/*', as: 'globalMessages' },
  ],
})

… and then would be able to use that through ‘variable’:

// input: pages/projects/index.vue/<script setup>
console.log(formatMessage(globalMessages.pageTitles.projects))
declare global {
  const globalMessages: Readonly<{
    pageTitles: typeof import('../../messages/page-titles.ts')['default']
    errors: typeof import('../../messages/errors.ts')['default']
    getWorldGreeting: typeof('../../messages/utils.ts')['getWorldGreeting']
  }>
}

… which would expand to something along the lines of:

// output: pages/projects/index.vue/<script setup>
import pageTitles from '../../messages/page-titles.ts'
console.log(formatMessage(pageTitles.projects))

It may also work with other language features like destructuring, as long as it's deterministic (did you know that something like this would blow up tree-shaking in rollup?):

// src/a.js
const { pageTitles: { projects: projectsTitle } } = globalMessages
// src/a.js [transformed]
import pageTitles from '../../messages/page-titles.ts'
const { projects: projectsTitle } = pageTitles

Additional information

varHarrie commented 8 months ago

When the number is large enough, all variables are exposed globally, which will indeed cause certain pollution to eslintrc globals and types, and can easily cause conflicts.

Hope there was a way to do something similar:

const code = `
$.foo();
$.bar();
`

const { injectImports } = createUnimport({
  imports: [
    { name: 'foo', from: 'my-lib', as: '$.foo' },
    { name: 'bar', from: 'my-lib', as: '$.bar' },
  ]
})

const { code } = await injectImports(code)

Output:

import { foo as $__foo, bar as $__bar } from 'my-lib';

const $ = { foo: $__foo, bar: $__bar };

$.foo();
$.bar();