Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add configurable NODE_PATH support and improve module resolution #46

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#Config helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org

root = true

[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ autocmd User Node
\ endif
```

#### Want to resolve modules with a custom NODE_PATH?

```vim
let g:vim_node#node_path = [$HOME.'/project/src', '/absolute/path']
```

Or you can also start vim with the `NODE_PATH` environment variable set.

License
-------
Expand Down
26 changes: 11 additions & 15 deletions autoload/node.vim
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
" Vim by default sets the filetype to JavaScript for the following suffices.
" And, yes, it has .jsx there.
let node#suffixesadd = [".js", ".json", ".es", ".jsx"]
if !exists("node#suffixesadd")
let node#suffixesadd = [".js", ".json", ".es", ".jsx"]
endif

function! node#initialize(root)
let b:node_root = a:root

command! -bar -bang -nargs=1 -buffer -complete=customlist,s:complete Nedit
\ exe s:nedit(<q-args>, bufname("%"), "edit<bang>")
\ exe s:nedit(<q-args>, expand('%:p'), "edit<bang>")
command! -bar -bang -nargs=1 -buffer -complete=customlist,s:complete Nopen
\ exe s:nopen(<q-args>, bufname("%"), "edit<bang>")
\ exe s:nopen(<q-args>, expand('%:p'), "edit<bang>")

nnoremap <buffer><silent> <Plug>NodeGotoFile
\ :call <SID>edit(expand("<cfile>"), bufname("%"))<CR>
\ :call <SID>edit(expand("<cfile>"), expand('%:p'))<CR>
nnoremap <buffer><silent> <Plug>NodeSplitGotoFile
\ :call <SID>edit(expand("<cfile>"), bufname("%"), "split")<CR>
\ :call <SID>edit(expand("<cfile>"), expand('%:p'), "split")<CR>
nnoremap <buffer><silent> <Plug>NodeVSplitGotoFile
\ :call <SID>edit(expand("<cfile>"), bufname("%"), "vsplit")<CR>
\ :call <SID>edit(expand("<cfile>"), expand('%:p'), "vsplit")<CR>
nnoremap <buffer><silent> <Plug>NodeTabGotoFile
\ :call <SID>edit(expand("<cfile>"), bufname("%"), "tab split")<CR>
\ :call <SID>edit(expand("<cfile>"), expand('%:p'), "tab split")<CR>

silent doautocmd User Node
endfunction
Expand All @@ -30,7 +32,7 @@ function! node#javascript()
setlocal path-=/usr/include
let &l:suffixesadd .= "," . join(g:node#suffixesadd, ",")
let &l:include = '\<require(\(["'']\)\zs[^\1]\+\ze\1'
let &l:includeexpr = "node#lib#find(v:fname, bufname('%'))"
let &l:includeexpr = "node#lib#find(v:fname, expand('%:p'))"

" @ is used for scopes, but isn't a default filename character on
" non-Windows sytems.
Expand All @@ -50,13 +52,7 @@ function! s:edit(name, from, ...)
if empty(a:name) | return | endif
let dir = isdirectory(a:from) ? a:from : fnamemodify(a:from, ":h")
let command = a:0 == 1 ? a:1 : "edit"

" If just a plain filename with no directory part, check if it exists:
if a:name !~# '^\v(/|\./|\.\./)' && filereadable(dir . "/" . a:name)
let path = dir . "/" . a:name
else
let path = node#lib#find(a:name, dir)
end
let path = node#lib#find(a:name, dir)

if empty(path)
return s:error("E447: Can't find file \"" . a:name . "\" in path")
Expand Down
159 changes: 129 additions & 30 deletions autoload/node/lib.vim
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ let s:CORE_MODULES = ["_debugger", "_http_agent", "_http_client",
\ "readline", "repl", "smalloc", "stream", "string_decoder", "sys",
\ "timers", "tls", "tty", "url", "util", "vm", "zlib"]

" A vimscript implementation of the node.js module resolution algorithm.
" https://nodejs.org/api/modules.html#modules_all_together
"
" require(a:name) from module at path a:from
" 1. If a:name is a core module,
" a. return the core module
" b. STOP
" 2. If a:name begins with '/'
" a. set a:from to be the filesystem root
" 3. If a:name begins with './' or '/' or '../'
" a. LOAD_AS_FILE(a:from + a:name)
" b. LOAD_AS_DIRECTORY(a:from + a:name)
" 4. LOAD_NODE_MODULES(a:name, dirname(a:from))
" 5. THROW "not found"
function! node#lib#find(name, from)
if index(s:CORE_MODULES, a:name) != -1
let l:version = node#lib#version()
Expand All @@ -22,39 +36,47 @@ function! node#lib#find(name, from)
return s:CORE_URL_PREFIX ."/". l:version ."/". l:dir ."/". a:name .".js"
endif

return s:resolve(s:absolutize(a:name, a:from))
endfunction
let request = s:getModulePath(a:name, a:from)
if !empty(request)
let asFile = s:loadAsFile(request)
if !empty(asFile) | return resolve(asFile) | endif

function! node#lib#version()
if exists("b:node_version") | return b:node_version | endif
if !executable("node") | let b:node_version = "" | return | endif
let b:node_version = matchstr(system("node --version"), '^v\?\zs[0-9.]\+')
return b:node_version
endfunction

function! s:absolutize(name, from)
if a:name =~# s:ABSPATH
return a:name
elseif a:name =~# s:RELPATH
let dir = isdirectory(a:from) ? a:from : fnamemodify(a:from, ":h")
return dir . "/" . a:name
else
return b:node_root . "/node_modules/" . a:name
let asDirectory = s:loadAsDirectory(request)
if !empty(asDirectory) | return resolve(asDirectory) | endif
endif

let asNodeModule = s:loadNodeModules(a:name, s:dirname(a:from))
if !empty(asNodeModule) | return resolve(asNodeModule) | endif
endfunction

function! s:resolve(path)
" Node checks for files *before* directories, so see if the path does not
" end with a slash or dots and try to match it as a file.
" LOAD_AS_FILE(X)
" 1. If X is a file, load X as JavaScript text. STOP
" 2. If X.js is a file, load X.js as JavaScript text. STOP
" 3. If X.json is a file, parse X.json to a JavaScript Object. STOP
" 4. If X.node is a file, load X.node as binary addon. STOP
function! s:loadAsFile(path)
if a:path !~# '\v/(\.\.?/?)?$'
let path_with_suffix = s:resolveSuffix(a:path)
if !empty(path_with_suffix) | return path_with_suffix | endif
endif
endfunction

if isdirectory(a:path) | return s:resolveFromDirectory(a:path) | endif
" LOAD_INDEX(X)
" 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
" 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
" 3. If X/index.node is a file, load X/index.node as binary addon. STOP
function! s:loadIndex(path)
return s:resolveSuffix(a:path . "/index")
endfunction

function! s:resolveFromDirectory(path)
" LOAD_AS_DIRECTORY(X)
" 1. If X/package.json is a file,
" a. Parse X/package.json, and look for "main" field.
" b. let M = X + (json main field)
" c. LOAD_AS_FILE(M)
" d. LOAD_INDEX(M)
" 2. LOAD_INDEX(X)
function! s:loadAsDirectory(path)
" Node.js checks for package.json in every directory, not just the
" module's parent. According to:
" http://nodejs.org/api/modules.html#modules_all_together
Expand All @@ -66,17 +88,94 @@ function! s:resolveFromDirectory(path)
let main = s:mainFromPackage(a:path . "/package.json")

if !empty(main) && main != ""
let path = s:resolve(a:path . "/" . main)
if !empty(path) | return path | endif
let path = a:path . "/" . main
let asFile = s:loadAsFile(path)
if !empty(asFile) | return asFile | endif

let asIndex = s:loadIndex(path)
if !empty(asIndex) | return asIndex | endif
endif
endif

" We need to check for ./index.js's existence here rather than leave it to
" the caller, because otherwise we can't distinguish if this ./index was
" from the directory defaulting to ./index.js or it was the package.json
" which referred to ./index, which in itself could mean both ./index.js and
" ./index/index.js.
return s:resolveSuffix(a:path . "/index")
return s:loadIndex(a:path)
endfunction

" LOAD_NODE_MODULES(X, START)
" 1. let DIRS=NODE_MODULES_PATHS(START)
" 2. for each DIR in DIRS:
" a. LOAD_AS_FILE(DIR/X)
" b. LOAD_AS_DIRECTORY(DIR/X)
function! s:loadNodeModules(x, start)
let dirs = s:nodeModulePaths(a:start)
for dir in dirs
let path = dir . "/" . a:x
let asFile = s:loadAsFile(path)
if !empty(asFile) | return asFile | endif

let asDirectory = s:loadAsDirectory(path)
if !empty(asDirectory) | return asDirectory | endif
endfor
endfunction

" NODE_MODULES_PATHS(START)
" 1. let PARTS = path split(START)
" 2. let I = count of PARTS - 1
" 3. let DIRS = []
" 4. while I >= 0,
" a. if PARTS[I] = "node_modules" CONTINUE
" b. DIR = path join(PARTS[0 .. I] + "node_modules")
" c. DIRS = DIRS + DIR
" d. let I = I - 1
" 5. return DIRS
function! s:nodeModulePaths(start)
let parts = split(a:start, '/')

" We want to keep the leading slash of an absolute path
if a:start =~# s:ABSPATH
let parts[0] = '/' . parts[0]
endif

let i = len(parts) - 1
let dirs = []
while i >= 0
if parts[i] == 'node_modules' | continue | endif
let dir = join(parts[0:i] + ['node_modules'], '/')
let dirs += [dir]
let i = i - 1
endwhile

" Add support for NODE_PATH
let NODE_PATH = $NODE_PATH
if !empty(NODE_PATH)
let dirs += [NODE_PATH]
endif

" Add support for configured NODE_PATH
if !empty(g:vim_node#node_path)
let dirs += g:vim_node#node_path
endif

return dirs
endfunction

function! s:getModulePath(name, from)
if a:name =~# s:ABSPATH
return a:name
elseif a:name =~# s:RELPATH
let dir = isdirectory(a:from) ? a:from : s:dirname(a:from)
return dir . "/" . a:name
endif
endfunction

function! s:dirname(path)
return fnamemodify(a:path, ':h')
endfunction

function! node#lib#version()
if exists("b:node_version") | return b:node_version | endif
if !executable("node") | let b:node_version = "" | return | endif
let b:node_version = matchstr(system("node --version"), '^v\?\zs[0-9.]\+')
return b:node_version
endfunction

function! s:mainFromPackage(path)
Expand Down