Skip to content

Commit 547044b

Browse files
committed
feat: auto reload on components change
1 parent 099d52f commit 547044b

File tree

12 files changed

+251
-136
lines changed

12 files changed

+251
-136
lines changed

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,21 @@
2222
"prepublish": "npm run build",
2323
"release": "standard-version && npm publish && git push"
2424
},
25+
"dependencies": {
26+
"debug": "^4.1.1",
27+
"fast-glob": "^3.2.4",
28+
"minimatch": "^3.0.4"
29+
},
2530
"devDependencies": {
2631
"@antfu/eslint-config": "^0.3.2",
2732
"@types/debug": "^4.1.5",
33+
"@types/minimatch": "^3.0.3",
2834
"@typescript-eslint/eslint-plugin": "^3.9.1",
2935
"eslint": "^7.7.0",
3036
"rollup": "^2.26.4",
3137
"standard-version": "^9.0.0",
3238
"tsup": "^3.6.1",
3339
"typescript": "^3.9.7",
3440
"vite": "^1.0.0-rc.4"
35-
},
36-
"dependencies": {
37-
"debug": "^4.1.1",
38-
"fast-glob": "^3.2.4"
39-
}
41+
}
4042
}

pnpm-lock.yaml

Lines changed: 15 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/context.ts

Lines changed: 116 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,139 @@
1+
import Debug from 'debug'
12
import { ComponentsInfo, ComponentsImportMap, Options } from './types'
2-
import { normalize } from './utils'
3+
import { normalize, toArray, getNameFromFilePath } from './utils'
4+
import { searchComponents } from './fs/glob'
5+
6+
const debug = {
7+
components: Debug('vite-plugin-components:context:components'),
8+
}
39

410
export class Context {
5-
importMap: ComponentsImportMap = {}
6-
_searchingPromise?: Promise<any>
11+
readonly root = process.cwd()
12+
readonly globs: string[]
713

8-
private _components: ComponentsInfo[] = []
9-
private importMapPromises: Record<string, [(null | Promise<string[]>), (null | ((result: string[]) => void))]> = {}
14+
private _componentPaths = new Set<string>()
15+
private _componentNameMap: Record<string, ComponentsInfo> = {}
16+
private _imports: ComponentsImportMap = {}
17+
private _importsResolveTasks: Record<string, [(null | Promise<string[]>), (null | ((result: string[]) => void))]> = {}
1018

1119
constructor(
1220
public readonly options: Options,
13-
) {}
21+
) {
22+
const { extensions, dirs, deep } = options
23+
const exts = toArray(extensions)
24+
25+
if (!exts.length)
26+
throw new Error('[vite-plugin-components] extensions are required to search for components')
27+
28+
const extsGlob = exts.length === 1 ? exts[0] : `{${exts.join(',')}}`
29+
this.globs = toArray(dirs).map(i =>
30+
deep
31+
? `${i}/**/*.${extsGlob}`
32+
: `${i}/*.${extsGlob}`,
33+
)
34+
35+
this.searchGlob()
36+
}
1437

15-
get components() {
16-
return this._components
38+
addComponents(paths: string | string[]) {
39+
const size = this._componentPaths.size
40+
toArray(paths).forEach(p => this._componentPaths.add(`/${p}`))
41+
if (this._componentPaths.size !== size) {
42+
this.updateComponentNameMap()
43+
return true
44+
}
45+
return false
1746
}
1847

19-
set components(components: ComponentsInfo[]) {
20-
this._components = components.map(([name, path]) => [normalize(name), path])
48+
removeComponents(paths: string | string[]) {
49+
const size = this._componentPaths.size
50+
toArray(paths).forEach(p => this._componentPaths.delete(`/${p}`))
51+
if (this._componentPaths.size !== size) {
52+
this.updateComponentNameMap()
53+
return true
54+
}
55+
return false
2156
}
2257

23-
searchForComponents(names: string[], excludePaths: string[] = []) {
24-
return this.components.filter(i => !excludePaths.includes(i[1]) && names.includes(i[0]))
58+
findReferencesOfComponentName(name: string) {
59+
return Object.entries(this._imports)
60+
.map(([path, components]) => components?.includes(name) ? path : undefined)
61+
.filter(Boolean) as string[]
2562
}
2663

27-
async getImportMap(key: string) {
28-
if (this.importMap[key])
29-
return this.importMap[key]
64+
private updateComponentNameMap() {
65+
this._componentNameMap = {}
66+
67+
debug.components(this._componentPaths)
68+
69+
Array
70+
.from(this._componentPaths)
71+
.forEach((path) => {
72+
const name = normalize(getNameFromFilePath(path))
73+
if (this._componentNameMap[name]) {
74+
console.warn(`[vite-plugin-components] component "${name}"(${path}) has naming conflicts with other components, ignored.`)
75+
return
76+
}
77+
this._componentNameMap[name] = { name, path }
78+
})
79+
}
3080

31-
if (!this.importMapPromises[key]) {
32-
this.importMapPromises[key] = [null, null]
81+
findComponents(names: string[], excludePaths: string[] = []) {
82+
return names
83+
.map((name) => {
84+
const info = this._componentNameMap[name]
85+
if (info && !excludePaths.includes(info.path))
86+
return info
87+
return undefined
88+
})
89+
.filter(Boolean) as ComponentsInfo[]
90+
}
91+
92+
setImports(key: string, names: string[]) {
93+
const casedNames = names.map(name => normalize(name))
94+
this._imports[key] = casedNames
95+
if (this._importsResolveTasks[key])
96+
this._importsResolveTasks[key][1]?.(casedNames)
97+
}
98+
99+
/**
100+
* Await for imports to get resolved
101+
*
102+
* @param ctx
103+
* @param force
104+
*/
105+
async getImports(key: string) {
106+
if (this._imports[key])
107+
return this._imports[key]
108+
109+
if (!this._importsResolveTasks[key]) {
110+
this._importsResolveTasks[key] = [null, null]
33111
const p = new Promise<string[]>((resolve) => {
34-
this.importMapPromises[key][1] = resolve
112+
this._importsResolveTasks[key][1] = resolve
35113
})
36-
this.importMapPromises[key][0] = p
114+
this._importsResolveTasks[key][0] = p
37115
}
38116

39-
return await Promise.resolve(this.importMapPromises[key][0])
117+
return await Promise.resolve(this._importsResolveTasks[key][0])
40118
}
41119

42-
setImportMap(key: string, names: string[]) {
43-
const casedNames = names.map(name => normalize(name))
44-
this.importMap[key] = casedNames
45-
if (this.importMapPromises[key])
46-
this.importMapPromises[key][1]?.(casedNames)
120+
private _searchGlob: Promise<void> | undefined
121+
122+
/**
123+
* This search for components in with the given options.
124+
* Will be called multiple times to ensure file loaded,
125+
* should normally run only once.
126+
*
127+
* @param ctx
128+
* @param force
129+
*/
130+
async searchGlob() {
131+
if (!this._searchGlob) {
132+
this._searchGlob = (async() => {
133+
await searchComponents(this)
134+
})()
135+
}
136+
137+
return await this._searchGlob
47138
}
48139
}

src/fs/glob.ts

Lines changed: 10 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,21 @@
1-
import path from 'path'
21
import fg from 'fast-glob'
32
import Debug from 'debug'
4-
import { ComponentsInfo } from '../types'
53
import { Context } from '../context'
64

75
const debug = Debug('vite-plugin-components:glob')
86

9-
function getNameFromFilePath(filePath: string): string {
10-
const parsedFilePath = path.parse(filePath)
11-
if (parsedFilePath.name === 'index') {
12-
const filePathSegments = filePath.split(path.sep)
13-
const parentDirName = filePathSegments[filePathSegments.length - 2]
14-
if (parentDirName)
15-
return parentDirName
16-
}
17-
return parsedFilePath.name
18-
}
19-
20-
function toArray<T>(arr: T | T[]): T[] {
21-
if (Array.isArray(arr))
22-
return arr
23-
return [arr]
24-
}
25-
26-
/**
27-
* This search for components in with the given options.
28-
* Will be called multiple times to ensure file loaded,
29-
* should normally run only once.
30-
*
31-
* TODO: watch on file changes for server mode
32-
*
33-
* @param ctx
34-
* @param force
35-
*/
36-
export async function searchComponents(ctx: Context, force = false) {
37-
if (force || !ctx._searchingPromise) {
38-
ctx._searchingPromise = (async() => {
39-
const { dirs, deep, extensions } = ctx.options
40-
const exts = toArray(extensions)
41-
42-
if (!exts.length)
43-
throw new Error('[vite-plugin-components] extensions are required to search for components')
44-
45-
const extsGlob = exts.length === 1 ? exts[0] : `{${exts.join(',')}}`
46-
const globs = toArray(dirs).map(i =>
47-
deep
48-
? `${i}/**/*.${extsGlob}`
49-
: `${i}/*.${extsGlob}`,
50-
)
51-
52-
debug(`searching start with: [${globs.join(', ')}]`)
53-
54-
const files = await fg(globs, {
55-
ignore: [
56-
'node_modules',
57-
],
58-
onlyFiles: true,
59-
})
60-
61-
if (!files.length)
62-
console.warn('[vite-plugin-components] no components found')
7+
export async function searchComponents(ctx: Context) {
8+
debug(`searching start with: [${ctx.globs.join(', ')}]`)
639

64-
const nameSets = new Set<string>()
65-
const components = files
66-
.map((f): ComponentsInfo => [getNameFromFilePath(f), `/${f}`])
67-
.filter(([name, path]) => {
68-
if (nameSets.has(name)) {
69-
console.warn(`[vite-plugin-components] component "${name}"(${path}) has naming conflicts with other components, ignored.`)
70-
return false
71-
}
72-
else {
73-
nameSets.add(name)
74-
return true
75-
}
76-
})
10+
const files = await fg(ctx.globs, {
11+
ignore: ['node_modules'],
12+
onlyFiles: true,
13+
})
7714

78-
debug(`${components.length} components found.`)
79-
debug(components.map(i => i[0]))
15+
if (!files.length)
16+
console.warn('[vite-plugin-components] no components found')
8017

81-
ctx.components = components
82-
})()
83-
}
18+
debug(`${files.length} components found.`)
8419

85-
await Promise.resolve(ctx._searchingPromise)
20+
ctx.addComponents(files)
8621
}

src/generator/resolver.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,18 @@ export function isResolverPath(reqPath: string) {
1010

1111
export async function generateResolver(ctx: Context, reqPath: string) {
1212
const sfcPath = reqPath.slice(0, -RESOLVER_EXT.length)
13-
const names = await ctx.getImportMap(sfcPath) || []
14-
const components = ctx.searchForComponents(names, [sfcPath])
15-
1613
debug(sfcPath)
17-
debug('using', names, 'imported', components.map(i => i[0]))
14+
15+
const names = await ctx.getImports(sfcPath) || []
16+
const components = ctx.findComponents(names, [sfcPath])
17+
18+
debug('using', names, 'imported', components.map(i => i.name))
1819

1920
return `
20-
${components.map(([name, path]) => `import ${name} from "${path}"`).join('\n')}
21+
${components.map(({ name, path }) => `import ${name} from "${path}"`).join('\n')}
2122
2223
export default (components) => {
23-
return Object.assign({}, { ${components.map(i => i[0]).join(', ')} }, components)
24+
return Object.assign({}, { ${components.map(i => i.name).join(', ')} }, components)
2425
}
2526
`
2627
}

0 commit comments

Comments
 (0)