Skip to content

Commit ab855c9

Browse files
committed
fix: warn when circular deps detected
1 parent 2ee316d commit ab855c9

File tree

5 files changed

+70
-26
lines changed

5 files changed

+70
-26
lines changed

index.mjs

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { builtinModules, createRequire } from 'module'
22
import { pathToFileURL } from 'url'
3-
import { join, dirname, resolve } from 'path'
3+
import { dirname, resolve, relative } from 'path'
44
import { createServer } from 'vite'
55
import createDebug from 'debug'
66
import minimist from 'minimist'
7-
import { red, dim } from 'kolorist'
7+
import { red, dim, yellow } from 'kolorist'
88

99
const argv = minimist(process.argv.slice(2), {
1010
alias: {
@@ -65,22 +65,43 @@ await server.close()
6565

6666
// --- CLI END ---
6767

68-
async function execute(files, server) {
69-
const cache = {}
68+
function normalizeId(id) {
69+
// Virtual modules start with `\0`
70+
if (id && id.startsWith('/@id/__x00__'))
71+
id = `\0${id.slice('/@id/__x00__'.length)}`
72+
if (id && id.startsWith('/@id/'))
73+
id = id.slice('/@id/'.length)
74+
return id
75+
}
7076

71-
async function request(id) {
72-
// Virtual modules start with `\0`
73-
if (id && id.startsWith('/@id/__x00__'))
74-
id = `\0${id.slice('/@id/__x00__'.length)}`
75-
if (id && id.startsWith('/@id/'))
76-
id = id.slice('/@id/'.length)
77+
function toFilePath(id) {
78+
const absolute = id.startsWith('/@fs/')
79+
? id.slice(4)
80+
: slash(resolve(server.config.root, id.slice(1)))
7781

78-
if (builtinModules.includes(id))
79-
return import(id)
82+
return absolute
83+
}
84+
85+
async function execute(files, server) {
86+
const __pendingModules__ = new Map()
87+
88+
async function directRequest(rawId, callstack) {
89+
if (builtinModules.includes(rawId))
90+
return import(rawId)
91+
92+
callstack = [...callstack, rawId]
93+
const request = async(dep) => {
94+
if (callstack.includes(dep)) {
95+
throw new Error(`${red('Circular dependency detected')}\nStack:\n${[...callstack, dep].reverse().map((i) => {
96+
const path = relative(server.config.root, toFilePath(normalizeId(i)))
97+
return dim(' -> ') + (i === dep ? yellow(path) : path)
98+
}).join('\n')}\n`)
99+
}
100+
return cachedRequest(dep, callstack)
101+
}
80102

81-
const absolute = id.startsWith('/@fs/')
82-
? id.slice(3)
83-
: slash(join(server.config.root, id.slice(1)))
103+
const id = normalizeId(rawId)
104+
const absolute = toFilePath(id)
84105

85106
debugRequest(absolute)
86107

@@ -89,7 +110,7 @@ async function execute(files, server) {
89110
? `/${absolute}`
90111
: absolute
91112

92-
if (id.includes('/node_modules/'))
113+
if (absolute.includes('/node_modules/'))
93114
return import(unifiedPath)
94115

95116
const result = await server.transformRequest(id, { ssr: true })
@@ -99,17 +120,16 @@ async function execute(files, server) {
99120
debugTransform(id, result.code)
100121

101122
const url = pathToFileURL(unifiedPath)
102-
103123
const exports = {}
104124

105125
const context = {
106126
require: createRequire(url),
107127
__filename: absolute,
108128
__dirname: dirname(absolute),
109-
__vite_ssr_import__: cachedRequest,
110-
__vite_ssr_dynamic_import__: cachedRequest,
129+
__vite_ssr_import__: request,
130+
__vite_ssr_dynamic_import__: request,
111131
__vite_ssr_exports__: exports,
112-
__vite_ssr_exportAll__: obj => Object.assign(exports, obj),
132+
__vite_ssr_exportAll__: obj => exportAll(exports, obj),
113133
__vite_ssr_import_meta__: { url },
114134
}
115135

@@ -119,21 +139,37 @@ async function execute(files, server) {
119139
)
120140

121141
// prefetch deps
122-
result.deps.forEach(dep => cachedRequest(dep))
142+
result.deps.forEach(dep => request(dep))
123143

124144
await fn(...Object.values(context))
125145
return exports
126146
}
127147

128-
function cachedRequest(path) {
129-
if (!cache[path])
130-
cache[path] = request(path)
131-
return cache[path]
148+
function cachedRequest(id, callstack) {
149+
if (!__pendingModules__[id])
150+
__pendingModules__[id] = directRequest(id, callstack)
151+
return __pendingModules__[id]
152+
}
153+
154+
function exportAll(exports, sourceModule) {
155+
// eslint-disable-next-line no-restricted-syntax
156+
for (const key in sourceModule) {
157+
if (key !== 'default') {
158+
try {
159+
Object.defineProperty(exports, key, {
160+
enumerable: true,
161+
configurable: true,
162+
get() { return sourceModule[key] },
163+
})
164+
}
165+
catch (_err) { }
166+
}
167+
}
132168
}
133169

134170
const result = []
135171
for (const file of files)
136-
result.push(await request(`/@fs/${slash(resolve(file))}`))
172+
result.push(await cachedRequest(`/@fs/${slash(resolve(file))}`, []))
137173
return result
138174
}
139175

test/fixtures/a.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import VueComponent from './component.vue'
22

33
export * from './b.js'
44

5+
// export * as circular from './circular/index.ts'
6+
57
export const dir = __dirname
68

79
export { VueComponent }

test/fixtures/circular/a.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const a = 1

test/fixtures/circular/b.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { a } from '.'
2+
3+
export const b = a * 2

test/fixtures/circular/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './b'
2+
export * from './a'

0 commit comments

Comments
 (0)