next-auto-import 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,287 @@
1
+ import { existsSync, promises as fs } from 'node:fs'
2
+ import { dirname, isAbsolute, relative, resolve } from 'node:path'
3
+ import process from 'node:process'
4
+ import { slash, toArray } from '@antfu/utils'
5
+ import { isPackageExists } from 'local-pkg'
6
+ import { createUnimport, resolvePreset } from 'unimport'
7
+ import { presets } from '../presets'
8
+ import { generateBiomeLintConfigs } from './biomelintrc'
9
+ import { generateESLintConfigs } from './eslintrc'
10
+ import { resolversAddon } from './resolvers'
11
+ import type { Import, InlinePreset } from 'unimport'
12
+ import type { BiomeLintrc, ESLintrc, Options } from '../types'
13
+
14
+ export const INCLUDE_RE_LIST = [/\.[jt]sx?$/]
15
+ export const EXCLUDE_RE_LIST = [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/]
16
+
17
+ export function createContext(options: Options = {}, root = process.cwd()) {
18
+ root = slash(root)
19
+
20
+ const {
21
+ dts: preferDTS = isPackageExists('typescript'),
22
+ dtsMode = 'append',
23
+ dtsPreserveExts = false,
24
+ dirsScanOptions,
25
+ dirs,
26
+ } = options
27
+
28
+ const eslintrc: ESLintrc = options.eslintrc || {}
29
+ eslintrc.enabled = eslintrc.enabled === undefined ? false : eslintrc.enabled
30
+ eslintrc.filepath = eslintrc.filepath || './.eslintrc-auto-import.json'
31
+ eslintrc.globalsPropValue =
32
+ eslintrc.globalsPropValue === undefined ? true : eslintrc.globalsPropValue
33
+
34
+ const biomelintrc: BiomeLintrc = options.biomelintrc || {}
35
+ biomelintrc.enabled = biomelintrc.enabled !== undefined
36
+ biomelintrc.filepath =
37
+ biomelintrc.filepath || './.biomelintrc-auto-import.json'
38
+
39
+ const dumpUnimportItems =
40
+ options.dumpUnimportItems === true
41
+ ? './.unimport-items.json'
42
+ : (options.dumpUnimportItems ?? false)
43
+
44
+ const resolvers = options.resolvers ? [options.resolvers].flat(2) : []
45
+
46
+ // When "options.injectAtEnd" is undefined or true, it's true.
47
+ const injectAtEnd = options.injectAtEnd !== false
48
+
49
+ const unimport = createUnimport({
50
+ imports: [],
51
+ presets:
52
+ options.packagePresets?.map((p) =>
53
+ typeof p === 'string' ? { package: p } : p,
54
+ ) ?? [],
55
+ dirsScanOptions: {
56
+ ...dirsScanOptions,
57
+ cwd: root,
58
+ },
59
+ dirs,
60
+ injectAtEnd,
61
+ parser: options.parser,
62
+ addons: {
63
+ addons: [
64
+ resolversAddon(resolvers),
65
+ {
66
+ name: 'unplugin-auto-import:dts',
67
+ declaration(dts) {
68
+ return `${`
69
+ /* eslint-disable */
70
+ /* prettier-ignore */
71
+ // @ts-nocheck
72
+ // noinspection JSUnusedGlobalSymbols
73
+ // Generated by unplugin-auto-import
74
+ // biome-ignore lint: disable
75
+ ${dts}`.trim()}\n`
76
+ },
77
+ },
78
+ ],
79
+ },
80
+ })
81
+
82
+ const importsPromise = flattenImports(options.imports).then((imports) => {
83
+ if (!imports.length && !resolvers.length && !dirs?.length)
84
+ console.warn(
85
+ '[auto-import] plugin installed but no imports has defined, see https://github.com/antfu/unplugin-auto-import#configurations for configurations',
86
+ )
87
+
88
+ const compare = (
89
+ left: string | undefined,
90
+ right: NonNullable<Options['ignore'] | Options['ignoreDts']>[number],
91
+ ) => {
92
+ return right instanceof RegExp ? right.test(left!) : right === left
93
+ }
94
+
95
+ options.ignore?.forEach((name) => {
96
+ const i = imports.find((i) => compare(i.as, name))
97
+ if (i) i.disabled = true
98
+ })
99
+
100
+ options.ignoreDts?.forEach((name) => {
101
+ const i = imports.find((i) => compare(i.as, name))
102
+ if (i) i.dtsDisabled = true
103
+ })
104
+
105
+ return unimport.getInternalContext().replaceImports(imports)
106
+ })
107
+
108
+ const dts =
109
+ preferDTS === false
110
+ ? false
111
+ : preferDTS === true
112
+ ? resolve(root, 'auto-imports.d.ts')
113
+ : resolve(root, preferDTS)
114
+
115
+ const multilineCommentsRE = /\/\*.*?\*\//gs
116
+ const singlelineCommentsRE = /\/\/.*$/gm
117
+ const dtsReg = /declare\s+global\s*\{(.*?)[\n\r]\}/s
118
+ function parseDTS(dts: string) {
119
+ dts = dts.replace(multilineCommentsRE, '').replace(singlelineCommentsRE, '')
120
+
121
+ const code = dts.match(dtsReg)?.[0]
122
+ if (!code) return
123
+
124
+ return Object.fromEntries(
125
+ Array.from(
126
+ code.matchAll(/['"]?(const\s*[^\s'"]+)['"]?\s*:\s*(.+?)[,;\r\n]/g),
127
+ ).map((i) => [i[1], i[2]]),
128
+ )
129
+ }
130
+
131
+ async function generateDTS(file: string) {
132
+ await importsPromise
133
+ const dir = dirname(file)
134
+ const originalContent = existsSync(file)
135
+ ? await fs.readFile(file, 'utf-8')
136
+ : ''
137
+ const originalDTS = parseDTS(originalContent)
138
+ const currentContent = await unimport.generateTypeDeclarations({
139
+ resolvePath: (i) => {
140
+ if (i.from.startsWith('.') || isAbsolute(i.from)) {
141
+ const related = slash(
142
+ dtsPreserveExts
143
+ ? relative(dir, i.from)
144
+ : relative(dir, i.from).replace(/\.ts(x)?$/, ''),
145
+ )
146
+ return !related.startsWith('.') ? `./${related}` : related
147
+ }
148
+ return i.from
149
+ },
150
+ })
151
+
152
+ if (dtsMode === 'append') {
153
+ const currentDTS = parseDTS(currentContent)!
154
+ if (originalDTS) {
155
+ Object.assign(originalDTS, currentDTS)
156
+
157
+ const dtsList = Object.keys(originalDTS)
158
+ .sort()
159
+ .map((k) => ` ${k}: ${originalDTS[k]}`)
160
+ return currentContent.replace(
161
+ dtsReg,
162
+ () => `declare global {\n${dtsList.join('\n')}\n}`,
163
+ )
164
+ }
165
+ }
166
+
167
+ return currentContent
168
+ }
169
+
170
+ async function generateESLint() {
171
+ return generateESLintConfigs(await unimport.getImports(), eslintrc)
172
+ }
173
+
174
+ async function generateBiomeLint() {
175
+ return generateBiomeLintConfigs(await unimport.getImports())
176
+ }
177
+
178
+ // eslint-disable-next-line unicorn/consistent-function-scoping
179
+ async function writeFile(filePath: string, content = '') {
180
+ await fs.mkdir(dirname(filePath), { recursive: true })
181
+ return await fs.writeFile(filePath, content, 'utf-8')
182
+ }
183
+
184
+ let lastDTS: string | undefined
185
+ let lastESLint: string | undefined
186
+ let lastBiomeLint: string | undefined
187
+ let lastUnimportItems: string | undefined
188
+
189
+ async function writeConfigFiles() {
190
+ const promises: any[] = []
191
+ if (dts) {
192
+ promises.push(
193
+ generateDTS(dts).then((content) => {
194
+ if (content !== lastDTS) {
195
+ lastDTS = content
196
+ return writeFile(dts, content)
197
+ }
198
+ }),
199
+ )
200
+ }
201
+ if (eslintrc.enabled && eslintrc.filepath) {
202
+ const filepath = eslintrc.filepath
203
+ promises.push(
204
+ generateESLint().then(async (content) => {
205
+ if (filepath.endsWith('.cjs')) content = `module.exports = ${content}`
206
+ else if (filepath.endsWith('.mjs') || filepath.endsWith('.js'))
207
+ content = `export default ${content}`
208
+
209
+ content = `${content}\n`
210
+ if (content.trim() !== lastESLint?.trim()) {
211
+ lastESLint = content
212
+ return writeFile(eslintrc.filepath!, content)
213
+ }
214
+ }),
215
+ )
216
+ }
217
+
218
+ if (biomelintrc.enabled) {
219
+ promises.push(
220
+ generateBiomeLint().then((content) => {
221
+ if (content !== lastBiomeLint) {
222
+ lastBiomeLint = content
223
+ return writeFile(biomelintrc.filepath!, content)
224
+ }
225
+ }),
226
+ )
227
+ }
228
+
229
+ if (dumpUnimportItems) {
230
+ promises.push(
231
+ unimport.getImports().then((items) => {
232
+ if (!dumpUnimportItems) return
233
+ const content = JSON.stringify(items, null, 2)
234
+ if (content !== lastUnimportItems) {
235
+ lastUnimportItems = content
236
+ return writeFile(dumpUnimportItems, content)
237
+ }
238
+ }),
239
+ )
240
+ }
241
+
242
+ return Promise.all(promises)
243
+ }
244
+
245
+ return {
246
+ writeConfigFiles,
247
+ }
248
+ }
249
+
250
+ export async function flattenImports(
251
+ map: Options['imports'],
252
+ ): Promise<Import[]> {
253
+ const promises = await Promise.all(
254
+ toArray(map).map(async (definition) => {
255
+ if (typeof definition === 'string') {
256
+ if (!presets[definition])
257
+ throw new Error(`[auto-import] preset ${definition} not found`)
258
+ const preset = presets[definition]
259
+ definition = preset
260
+ }
261
+
262
+ if ('from' in definition && 'imports' in definition) {
263
+ return await resolvePreset(definition as InlinePreset)
264
+ } else {
265
+ const resolved: Import[] = []
266
+ for (const mod of Object.keys(definition)) {
267
+ for (const id of definition[mod]) {
268
+ const meta = {
269
+ from: mod,
270
+ } as Import
271
+ if (Array.isArray(id)) {
272
+ meta.name = id[0]
273
+ meta.as = id[1]
274
+ } else {
275
+ meta.name = id
276
+ meta.as = id
277
+ }
278
+ resolved.push(meta)
279
+ }
280
+ }
281
+ return resolved
282
+ }
283
+ }),
284
+ )
285
+
286
+ return promises.flat()
287
+ }
@@ -0,0 +1,16 @@
1
+ import type { Import } from 'unimport'
2
+ import type { ESLintrc } from '../types'
3
+
4
+ export function generateESLintConfigs(imports: Import[], eslintrc: ESLintrc) {
5
+ const eslintConfigs: any = { globals: {} }
6
+
7
+ imports
8
+ .map((i) => i.as ?? i.name)
9
+ .filter(Boolean)
10
+ .sort()
11
+ .forEach((name) => {
12
+ eslintConfigs.globals[name] = eslintrc.globalsPropValue
13
+ })
14
+ const jsonBody = JSON.stringify(eslintConfigs, null, 2)
15
+ return jsonBody
16
+ }
@@ -0,0 +1,94 @@
1
+ import { toArray } from '@antfu/utils'
2
+ import type { Addon, Import } from 'unimport'
3
+ import type {
4
+ ImportExtended,
5
+ ImportLegacy,
6
+ Resolver,
7
+ ResolverResult,
8
+ } from '../types'
9
+
10
+ export function normalizeImport(
11
+ info: Import | ResolverResult | ImportExtended | ImportLegacy | string,
12
+ name: string,
13
+ ): ImportExtended {
14
+ if (typeof info === 'string') {
15
+ return {
16
+ name: 'default',
17
+ as: name,
18
+ from: info,
19
+ }
20
+ }
21
+ if ('path' in info) {
22
+ return {
23
+ from: info.path,
24
+ as: info.name,
25
+ name: info.importName!,
26
+ sideEffects: info.sideEffects,
27
+ }
28
+ }
29
+ return {
30
+ name,
31
+ as: name,
32
+ ...info,
33
+ }
34
+ }
35
+
36
+ export async function firstMatchedResolver(
37
+ resolvers: Resolver[],
38
+ fullname: string,
39
+ ) {
40
+ let name = fullname
41
+ for (const resolver of resolvers) {
42
+ if (typeof resolver === 'object' && resolver.type === 'directive') {
43
+ if (name.startsWith('v')) name = name.slice(1)
44
+ else continue
45
+ }
46
+ const resolved = await (typeof resolver === 'function'
47
+ ? resolver(name)
48
+ : resolver.resolve(name))
49
+ if (resolved) return normalizeImport(resolved, fullname)
50
+ }
51
+ }
52
+
53
+ export function resolversAddon(resolvers: Resolver[]): Addon {
54
+ return {
55
+ name: 'unplugin-auto-import:resolvers',
56
+ async matchImports(names, matched) {
57
+ if (!resolvers.length) return
58
+ const dynamic: ImportExtended[] = []
59
+ const sideEffects: ImportExtended[] = []
60
+ await Promise.all(
61
+ [...names].map(async (name) => {
62
+ const matchedImport = matched.find((i) => i.as === name)
63
+ if (matchedImport) {
64
+ if ('sideEffects' in matchedImport)
65
+ sideEffects.push(
66
+ ...toArray((matchedImport as ImportExtended).sideEffects).map(
67
+ (i) => normalizeImport(i, ''),
68
+ ),
69
+ )
70
+
71
+ return
72
+ }
73
+ const resolved = await firstMatchedResolver(resolvers, name)
74
+ if (resolved) dynamic.push(resolved)
75
+
76
+ if (resolved?.sideEffects)
77
+ sideEffects.push(
78
+ ...toArray(resolved?.sideEffects).map((i) =>
79
+ normalizeImport(i, ''),
80
+ ),
81
+ )
82
+ }),
83
+ )
84
+
85
+ if (dynamic.length) {
86
+ this.dynamicImports.push(...dynamic)
87
+ this.invalidate()
88
+ }
89
+
90
+ if (dynamic.length || sideEffects.length)
91
+ return [...matched, ...dynamic, ...sideEffects]
92
+ },
93
+ }
94
+ }
@@ -0,0 +1,80 @@
1
+ import { slash } from '@antfu/utils'
2
+ import { isPackageExists } from 'local-pkg'
3
+ import pm from 'picomatch'
4
+ import { createUnplugin } from 'unplugin'
5
+ import { createContext, EXCLUDE_RE_LIST, INCLUDE_RE_LIST } from './ctx'
6
+ import type { FilterPattern } from 'unplugin'
7
+ import type { Options } from '../types'
8
+
9
+ export default createUnplugin<Options>((options) => {
10
+ let ctx = createContext(options)
11
+ return {
12
+ name: 'unplugin-auto-import',
13
+ enforce: 'post',
14
+ transformInclude(id) {
15
+ return ctx.filter(id)
16
+ },
17
+ transform: {
18
+ filter: {
19
+ id: {
20
+ include: (options.include as FilterPattern) || INCLUDE_RE_LIST,
21
+ exclude: (options.exclude as FilterPattern) || EXCLUDE_RE_LIST,
22
+ },
23
+ },
24
+ async handler(code, id) {
25
+ return ctx.transform(code, id)
26
+ },
27
+ },
28
+ async buildStart() {
29
+ await ctx.scanDirs()
30
+ },
31
+ async buildEnd() {
32
+ await ctx.writeConfigFiles()
33
+ },
34
+ vite: {
35
+ async config(config) {
36
+ if (options.viteOptimizeDeps === false) return
37
+
38
+ const exclude = config.optimizeDeps?.exclude || []
39
+
40
+ const imports = new Set(
41
+ (await ctx.unimport.getImports())
42
+ .map((i) => i.from)
43
+ .filter(
44
+ (i) =>
45
+ i.match(/^[a-z@]/) &&
46
+ !exclude.includes(i) &&
47
+ isPackageExists(i),
48
+ ),
49
+ )
50
+
51
+ if (!imports.size) return
52
+
53
+ return {
54
+ optimizeDeps: {
55
+ include: [...imports],
56
+ },
57
+ }
58
+ },
59
+ async handleHotUpdate({ file }) {
60
+ if (!ctx.dirs?.length) return
61
+
62
+ if (ctx.configFilePaths.includes(file)) return
63
+
64
+ const normalizedFilePath = slash(file)
65
+
66
+ const shouldRescan = ctx.normalizedDirPaths.some((dirPath) =>
67
+ pm.isMatch(normalizedFilePath, dirPath.glob),
68
+ )
69
+
70
+ if (shouldRescan) await ctx.scanDirs()
71
+ },
72
+ async configResolved(config) {
73
+ if (ctx.root !== config.root) {
74
+ ctx = createContext(options, config.root)
75
+ await ctx.scanDirs()
76
+ }
77
+ },
78
+ },
79
+ }
80
+ })
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { createContext } from './core/ctx'
2
+ import type { NextConfig } from 'next'
3
+ import type { Options } from './types'
4
+
5
+ /**
6
+ * Creates a Next.js auto-import context
7
+ */
8
+ export function createAutoImport(options: Options = {}) {
9
+ const ctx = createContext(options)
10
+
11
+ ctx.writeConfigFiles()
12
+
13
+ return (nextConfig: NextConfig = {}): NextConfig => {
14
+ return {
15
+ ...nextConfig,
16
+ experimental: {
17
+ ...nextConfig.experimental,
18
+ swcPlugins: [
19
+ [
20
+ 'swc-plugin-auto-import',
21
+ {
22
+ presets: ['react'],
23
+ },
24
+ ],
25
+ ...(nextConfig.experimental?.swcPlugins ?? []),
26
+ ],
27
+ },
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,9 @@
1
+ import react from './react'
2
+ import reactDom from './react-dom'
3
+
4
+ export const presets = {
5
+ 'react': react,
6
+ 'react-dom': reactDom,
7
+ }
8
+
9
+ export type PresetName = keyof typeof presets
@@ -0,0 +1,15 @@
1
+ import type { ImportsMap } from '../types'
2
+
3
+ export default {
4
+ 'react-dom': [
5
+ 'useFormStatus',
6
+ 'createPortal',
7
+ 'flushSync',
8
+ 'preconnect',
9
+ 'prefetchDNS',
10
+ 'preinit',
11
+ 'preinitModule',
12
+ 'preload',
13
+ 'preloadModule',
14
+ ],
15
+ } as const satisfies ImportsMap
@@ -0,0 +1,40 @@
1
+ import type { ImportsMap } from '../types'
2
+
3
+ export const CommonReactAPI = [
4
+ 'useState',
5
+ 'useCallback',
6
+ 'useMemo',
7
+ 'useEffect',
8
+ 'useRef',
9
+ 'useContext',
10
+ 'useReducer',
11
+ 'useImperativeHandle',
12
+ 'useDebugValue',
13
+ 'useDeferredValue',
14
+ 'useLayoutEffect',
15
+ 'useTransition',
16
+ 'startTransition',
17
+ 'useSyncExternalStore',
18
+ 'useInsertionEffect',
19
+ 'useId',
20
+ 'lazy',
21
+ 'memo',
22
+ 'createRef',
23
+ 'forwardRef',
24
+ ] as const
25
+
26
+ export default {
27
+ react: [
28
+ ...CommonReactAPI,
29
+ 'cache',
30
+ 'cacheSignal',
31
+ 'createContext',
32
+ 'use',
33
+ 'useOptimistic',
34
+ 'useEffectEvent',
35
+ 'useActionState',
36
+ 'Fragment',
37
+ 'Suspense',
38
+ 'Activity',
39
+ ],
40
+ } as const satisfies ImportsMap