Skip to content

Commit

Permalink
feat: support aliasing directories
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed May 28, 2020
1 parent 8e41db2 commit 801951e
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 33 deletions.
10 changes: 9 additions & 1 deletion playground/TestAlias.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
<template>
<h2>Alias</h2>
<p class="alias">{{ msg }}</p>
<p class="dir-alias">{{ dirMsg }}</p>
<p class="dir-alias-index">{{ dirIndexMsg }}</p>
</template>

<script>
import { msg } from 'alias'
import { msg as dirMsg } from '/@alias/named'
import { msg as dirIndexMsg } from '/@alias/'
export default {
data: () => ({ msg })
data: () => ({
msg,
dirMsg,
dirIndexMsg
})
}
</script>
1 change: 1 addition & 0 deletions playground/aliased-dir/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = 'directory alias index works.'
1 change: 1 addition & 0 deletions playground/aliased-dir/named.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const msg = 'directory alias works.'
3 changes: 2 additions & 1 deletion playground/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { jsPlugin } from './plugins/jsPlugin'

const config: UserConfig = {
alias: {
alias: '/aliased'
alias: '/aliased',
'/@alias/': require('path').resolve(__dirname, 'aliased-dir')
},
jsx: 'preact',
minify: false,
Expand Down
9 changes: 8 additions & 1 deletion src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,21 @@ export interface SharedConfig {
*/
root?: string
/**
* Import alias. Can only be exact mapping, does not support wildcard syntax.
* Import alias. The entries can either be exact request -> request mappings
* (exact, no wildcard syntax), or request path -> fs directory mappings.
* When using directory mappings, the key **must start and end with a slash**.
*
* Example `vite.config.js`:
* ``` js
* module.exports = {
* alias: {
* // alias package names
* 'react': '@pika/react',
* 'react-dom': '@pika/react-dom'
*
* // alias a path to a fs directory
* // the key must start and end with a slash
* '/@foo/': path.resolve(__dirname, 'some-special-dir')
* }
* }
* ```
Expand Down
83 changes: 70 additions & 13 deletions src/node/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import path from 'path'
import slash from 'slash'
import { cleanUrl, resolveFrom, queryRE } from './utils'
import {
idToFileMap,
moduleRE,
fileToRequestMap
moduleIdToFileMap,
moduleFileToIdMap
} from './server/serverPluginModuleResolve'
import { resolveOptimizedCacheDir } from './depOptimizer'
import chalk from 'chalk'
Expand All @@ -15,7 +15,7 @@ const debug = require('debug')('vite:resolve')
export interface Resolver {
requestToFile?(publicPath: string, root: string): string | undefined
fileToRequest?(filePath: string, root: string): string | undefined
alias?(id: string): string | undefined
alias?: ((id: string) => string | undefined) | Record<string, string>
}

export interface InternalResolver {
Expand All @@ -31,7 +31,7 @@ export const mainFields = ['module', 'jsnext', 'jsnext:main', 'browser', 'main']
const defaultRequestToFile = (publicPath: string, root: string): string => {
if (moduleRE.test(publicPath)) {
const id = publicPath.replace(moduleRE, '')
const cachedNodeModule = idToFileMap.get(id)
const cachedNodeModule = moduleIdToFileMap.get(id)
if (cachedNodeModule) {
return cachedNodeModule
}
Expand All @@ -43,7 +43,7 @@ const defaultRequestToFile = (publicPath: string, root: string): string => {
// try to resolve from normal node_modules
const nodeModule = resolveNodeModuleFile(root, id)
if (nodeModule) {
idToFileMap.set(id, nodeModule)
moduleIdToFileMap.set(id, nodeModule)
return nodeModule
}
}
Expand All @@ -55,7 +55,7 @@ const defaultRequestToFile = (publicPath: string, root: string): string => {
}

const defaultFileToRequest = (filePath: string, root: string): string => {
const moduleRequest = fileToRequestMap.get(filePath)
const moduleRequest = moduleFileToIdMap.get(filePath)
if (moduleRequest) {
return moduleRequest
}
Expand Down Expand Up @@ -94,17 +94,66 @@ const resolveExt = (id: string): string | undefined => {
}
}

const isDir = (p: string) => fs.existsSync(p) && fs.statSync(p).isDirectory()

export function createResolver(
root: string,
resolvers: Resolver[] = [],
alias: Record<string, string> = {}
userAlias: Record<string, string> = {}
): InternalResolver {
function resolveRequest(
resolvers = [...resolvers]
const literalAlias: Record<string, string> = {}

const resolveAlias = (alias: Record<string, string>) => {
for (const key in alias) {
let target = alias[key]
// aliasing a directory
if (key.startsWith('/') && key.endsWith('/') && path.isAbsolute(target)) {
// check first if this is aliasing to a path from root
const fromRoot = path.join(root, target)
if (isDir(fromRoot)) {
target = fromRoot
} else if (!isDir(target)) {
continue
}
resolvers.push({
requestToFile(publicPath) {
if (publicPath.startsWith(key)) {
return path.join(target, publicPath.slice(key.length))
}
},
fileToRequest(filePath) {
if (filePath.startsWith(target)) {
return slash(key + path.relative(target, filePath))
}
}
})
} else {
literalAlias[key] = target
}
}
}

resolvers.forEach((r) => {
if (r.alias && typeof r.alias === 'object') {
resolveAlias(r.alias)
}
})
resolveAlias(userAlias)

const requestToFileCache = new Map()
const fileToRequestCache = new Map()

const resolveRequest = (
publicPath: string
): {
filePath: string
ext: string | undefined
} {
} => {
if (requestToFileCache.has(publicPath)) {
return requestToFileCache.get(publicPath)
}

let resolved: string | undefined
for (const r of resolvers) {
const filepath = r.requestToFile && r.requestToFile(publicPath, root)
Expand All @@ -117,10 +166,12 @@ export function createResolver(
resolved = defaultRequestToFile(publicPath, root)
}
const ext = resolveExt(resolved)
return {
const result = {
filePath: ext ? resolved + ext : resolved,
ext
}
requestToFileCache.set(publicPath, result)
return result
}

return {
Expand All @@ -133,20 +184,26 @@ export function createResolver(
},

fileToRequest(filePath) {
if (fileToRequestCache.has(filePath)) {
return fileToRequestCache.get(filePath)
}
for (const r of resolvers) {
const request = r.fileToRequest && r.fileToRequest(filePath, root)
if (request) return request
}
return defaultFileToRequest(filePath, root)
const res = defaultFileToRequest(filePath, root)
fileToRequestCache.set(filePath, res)
return res
},

alias(id) {
let aliased: string | undefined = alias[id]
let aliased: string | undefined = literalAlias[id]
if (aliased) {
return aliased
}
for (const r of resolvers) {
aliased = r.alias && r.alias(id)
aliased =
r.alias && typeof r.alias === 'function' ? r.alias(id) : undefined
if (aliased) {
return aliased
}
Expand Down
10 changes: 5 additions & 5 deletions src/node/server/serverPluginModuleResolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { resolveOptimizedModule, resolveNodeModuleFile } from '../resolver'

const debug = require('debug')('vite:resolve')

export const idToFileMap = new Map()
export const fileToRequestMap = new Map()
export const moduleIdToFileMap = new Map()
export const moduleFileToIdMap = new Map()

export const moduleRE = /^\/@modules\//

Expand All @@ -30,8 +30,8 @@ export const moduleResolvePlugin: ServerPlugin = ({ root, app, watcher }) => {
ctx.type = 'js'

const serve = async (id: string, file: string, type: string) => {
idToFileMap.set(id, file)
fileToRequestMap.set(file, ctx.path)
moduleIdToFileMap.set(id, file)
moduleFileToIdMap.set(file, ctx.path)
debug(`(${type}) ${id} -> ${getDebugPath(root, file)}`)
await cachedRead(ctx, file)

Expand All @@ -49,7 +49,7 @@ export const moduleResolvePlugin: ServerPlugin = ({ root, app, watcher }) => {
}

// already resolved and cached
const cachedPath = idToFileMap.get(id)
const cachedPath = moduleIdToFileMap.get(id)
if (cachedPath) {
return serve(id, cachedPath, 'cached')
}
Expand Down
4 changes: 4 additions & 0 deletions src/node/server/serverPluginModuleRewrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,10 @@ export const resolveImport = (
const ext = resolver.resolveExt(pathname)
if (ext) {
pathname += ext
if (ext[0] === '/') {
// in aliased cases the inferred ext can contain multiple slashes
pathname = pathname.replace(/\/\/+/g, '/')
}
}

// 4. mark non-src imports
Expand Down
19 changes: 9 additions & 10 deletions src/node/server/serverPluginServeStatic.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fs from 'fs'
import path from 'path'
import { ServerPlugin } from '.'
import { isStaticAsset } from '../utils'
Expand Down Expand Up @@ -34,16 +35,6 @@ export const serveStaticPlugin: ServerPlugin = ({
}
app.use(require('koa-etag')())

app.use((ctx, next) => {
const redirect = resolver.requestToFile(ctx.path)
if (!redirect.startsWith(root)) {
// resolver resolved to a file that is outside of project root,
// manually send here
return send(ctx, redirect, { root: '/' })
}
return next()
})

app.use((ctx, next) => {
if (ctx.path.startsWith('/public/') && isStaticAsset(ctx.path)) {
console.error(
Expand All @@ -55,6 +46,14 @@ export const serveStaticPlugin: ServerPlugin = ({
)
)
}
const filePath = resolver.requestToFile(ctx.path)
if (
filePath !== ctx.path &&
fs.existsSync(filePath) &&
fs.statSync(filePath).isFile()
) {
return send(ctx, filePath, { root: '/' })
}
return next()
})
app.use(require('koa-static')(root))
Expand Down
4 changes: 2 additions & 2 deletions src/node/utils/pathUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export const resolveRelativeRequest = (importer: string, id: string) => {
const resolved = slash(path.posix.resolve(path.dirname(importer), id))
const queryMatch = id.match(queryRE)
return {
url: resolved,
pathname: cleanUrl(resolved),
// path resovle strips ending / which should be preserved
pathname: cleanUrl(resolved) + (id.endsWith('/') ? '/' : ''),
query: queryMatch ? queryMatch[0] : ''
}
}
Expand Down
18 changes: 18 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,29 @@ describe('vite', () => {

test('alias', async () => {
expect(await getText('.alias')).toMatch('alias works')
expect(await getText('.dir-alias')).toMatch('directory alias works')
expect(await getText('.dir-alias-index')).toMatch(
'directory alias index works'
)
if (!isBuild) {
await updateFile('aliased/index.js', (c) =>
c.replace('works', 'hmr works')
)
await expectByPolling(() => getText('.alias'), 'alias hmr works')
await updateFile('aliased-dir/named.js', (c) =>
c.replace('works', 'hmr works')
)
await expectByPolling(
() => getText('.dir-alias'),
'directory alias hmr works'
)
await updateFile('aliased-dir/index.js', (c) =>
c.replace('works', 'hmr works')
)
await expectByPolling(
() => getText('.dir-alias-index'),
'directory alias index hmr works'
)
}
})

Expand Down

0 comments on commit 801951e

Please sign in to comment.