Skip to content

Commit 3f70202

Browse files
committed
feat: add optional option filename
1 parent 4431482 commit 3f70202

File tree

7 files changed

+202
-23
lines changed

7 files changed

+202
-23
lines changed

__tests__/cjs/import.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os from 'os'
12
import path from 'path'
23
import {
34
importFromString,
@@ -130,6 +131,42 @@ export default code
130131
)
131132
expect(res.default).toBe(modulePath)
132133
})
134+
135+
it('should use relative filename in error stack trace', async () => {
136+
expect.assertions(1)
137+
const filename = 'foo.js'
138+
const relativeDirname = path.relative(process.cwd(), __dirname)
139+
const relativeFilename = path.join(relativeDirname, filename)
140+
try {
141+
await importFromStringFn('throw new Error("boom")', {
142+
filename,
143+
useCurrentGlobal: true
144+
})
145+
} catch (err) {
146+
if (err instanceof Error) {
147+
expect(err.stack).toMatch(new RegExp(`at \\S+${relativeFilename}:\\d+:\\d+$`, 'm'))
148+
} else {
149+
throw err
150+
}
151+
}
152+
})
153+
154+
it('should use absolute filename in error stack trace', async () => {
155+
expect.assertions(1)
156+
const filename = path.join(os.homedir(), 'foo', 'bar', 'baz.js')
157+
try {
158+
await importFromStringFn('throw new Error("boom")', {
159+
filename,
160+
useCurrentGlobal: true
161+
})
162+
} catch (err) {
163+
if (err instanceof Error) {
164+
expect(err.stack).toMatch(new RegExp(`at ${filename}:\\d+:\\d+$`, 'm'))
165+
} else {
166+
throw err
167+
}
168+
}
169+
})
133170
}
134171

135172
describe('importFromString', () => {

__tests__/cjs/require.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os from 'os'
12
import path from 'path'
23
import { requireFromString, createRequireFromString } from '../../src/index'
34

@@ -86,3 +87,39 @@ it('should be able to override current global', () => {
8687
expect(requireFromStringFn('module.exports = new Error()')).toBeInstanceOf(Array)
8788
expect(requireFromStringFn('module.exports = new global.Error()')).toBeInstanceOf(Array)
8889
})
90+
91+
it('should use relative filename in error stack trace', () => {
92+
expect.assertions(1)
93+
const filename = 'foo.js'
94+
const relativeDirname = path.relative(process.cwd(), __dirname)
95+
const relativeFilename = path.join(relativeDirname, filename)
96+
try {
97+
requireFromString('throw new Error("boom")', {
98+
filename,
99+
useCurrentGlobal: true
100+
})
101+
} catch (err) {
102+
if (err instanceof Error) {
103+
expect(err.stack).toMatch(new RegExp(`at \\S+${relativeFilename}:\\d+:\\d+$`, 'm'))
104+
} else {
105+
throw err
106+
}
107+
}
108+
})
109+
110+
it('should use absolute filename in error stack trace', () => {
111+
expect.assertions(1)
112+
const filename = path.join(os.homedir(), 'foo', 'bar', 'baz.js')
113+
try {
114+
requireFromString('throw new Error("boom")', {
115+
filename,
116+
useCurrentGlobal: true
117+
})
118+
} catch (err) {
119+
if (err instanceof Error) {
120+
expect(err.stack).toMatch(new RegExp(`at ${filename}:\\d+:\\d+$`, 'm'))
121+
} else {
122+
throw err
123+
}
124+
}
125+
})

__tests__/esm/import.test.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os from 'os'
12
import path from 'path'
23
import { fileURLToPath, pathToFileURL } from 'url'
34
import { importFromString, createImportFromString, importFromStringSync } from '../../src/index'
@@ -68,8 +69,8 @@ export default code
6869
expect((await importModule(pathToFileURL(absoluteModulePath).toString())).greet).toBe('hi')
6970
})
7071

71-
it('should be able to access __dirname and __filename', () => {
72-
const res = importFromStringSync(`
72+
it('should be able to access __dirname and __filename', async () => {
73+
const res = await importFromString(`
7374
export const dirname = __dirname
7475
export const filename = __filename
7576
`)
@@ -134,6 +135,43 @@ export default code
134135
})
135136
expect(res.default()).toBe('hi')
136137
})
138+
139+
it('should use relative filename in error stack trace', async () => {
140+
expect.assertions(1)
141+
const filename = 'foo.js'
142+
const relativeDirname = path.relative(process.cwd(), __dirname)
143+
const relativeFilename = path.join(relativeDirname, filename)
144+
try {
145+
await importFromString('throw new Error("boom")', {
146+
filename,
147+
useCurrentGlobal: true
148+
})
149+
} catch (err) {
150+
if (err instanceof Error) {
151+
expect(err.stack).toMatch(new RegExp(`at file://\\S+${relativeFilename}:\\d+:\\d+$`, 'm'))
152+
} else {
153+
throw err
154+
}
155+
}
156+
})
157+
158+
it('should use absolute filename in error stack trace', async () => {
159+
expect.assertions(1)
160+
const filenamePath = path.join(os.homedir(), 'foo', 'bar', 'baz.js')
161+
const filename = pathToFileURL(filenamePath).toString()
162+
try {
163+
await importFromString('throw new Error("boom")', {
164+
filename,
165+
useCurrentGlobal: true
166+
})
167+
} catch (err) {
168+
if (err instanceof Error) {
169+
expect(err.stack).toMatch(new RegExp(`at ${filename}:\\d+:\\d+$`, 'm'))
170+
} else {
171+
throw err
172+
}
173+
}
174+
})
137175
})
138176

139177
describe('importFromStringSync', () => {

__tests__/esm/require.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os from 'os'
12
import path from 'path'
23
import { fileURLToPath } from 'url'
34
import { requireFromString } from '../../src/index'
@@ -80,3 +81,39 @@ it('should work with current global', () => {
8081
})
8182
expect(res).toBeInstanceOf(Error)
8283
})
84+
85+
it('should use relative filename in error stack trace', () => {
86+
expect.assertions(1)
87+
const filename = 'foo.js'
88+
const relativeDirname = path.relative(process.cwd(), __dirname)
89+
const relativeFilename = path.join(relativeDirname, filename)
90+
try {
91+
requireFromString('throw new Error("boom")', {
92+
filename,
93+
useCurrentGlobal: true
94+
})
95+
} catch (err) {
96+
if (err instanceof Error) {
97+
expect(err.stack).toMatch(new RegExp(`at \\S+${relativeFilename}:\\d+:\\d+$`, 'm'))
98+
} else {
99+
throw err
100+
}
101+
}
102+
})
103+
104+
it('should use absolute filename in error stack trace', () => {
105+
expect.assertions(1)
106+
const filename = path.join(os.homedir(), 'foo', 'bar', 'baz.js')
107+
try {
108+
requireFromString('throw new Error("boom")', {
109+
filename,
110+
useCurrentGlobal: true
111+
})
112+
} catch (err) {
113+
if (err instanceof Error) {
114+
expect(err.stack).toMatch(new RegExp(`at ${filename}:\\d+:\\d+$`, 'm'))
115+
} else {
116+
throw err
117+
}
118+
}
119+
})

src/import.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { join } from 'path'
21
import vm, { createContext } from 'vm'
32
import { TransformOptions, transform, transformSync } from 'esbuild'
43
import { nanoid } from 'nanoid/async'
54
import { Options, requireFromString } from './require'
65
import {
76
isVMModuleAvailable,
8-
pathToFileURLString,
7+
ensureFileURL,
8+
ensurePath,
99
getCallerDirname,
10+
getModuleFilename,
1011
createGlobalObject,
1112
createContextObject,
1213
resolveModuleSpecifier
@@ -77,16 +78,21 @@ Enable '--experimental-vm-modules' CLI option or replace it with dynamic 'import
7778
}))
7879
}
7980

80-
const { dirname = getCallerDirname(), globals = {}, useCurrentGlobal = false } = options
81+
const {
82+
filename = `${await nanoid()}.js`,
83+
dirname = getCallerDirname(),
84+
globals = {},
85+
useCurrentGlobal = false
86+
} = options
8187

82-
const moduleFilename = join(dirname, `${await nanoid()}.js`)
83-
const moduleFileURLString = pathToFileURLString(moduleFilename)
88+
const moduleFilename = getModuleFilename(dirname, filename)
89+
const moduleFileURLString = ensureFileURL(moduleFilename)
8490

8591
const globalObject = createGlobalObject(globals, useCurrentGlobal)
8692
const contextObject = createContextObject(
8793
{
88-
__dirname: dirname,
89-
__filename: moduleFilename
94+
__dirname: ensurePath(dirname),
95+
__filename: ensurePath(moduleFilename)
9096
},
9197
globalObject
9298
)

src/require.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
11
import { Module, createRequire } from 'module'
2-
import { join } from 'path'
32
import { Context, runInNewContext } from 'vm'
43
import { nanoid } from 'nanoid'
54
import {
65
isInESModuleScope,
6+
ensurePath,
77
getCallerDirname,
8+
getModuleFilename,
89
createGlobalObject,
910
createContextObject,
1011
resolveModuleSpecifier
1112
} from './utils'
1213

1314
export interface Options {
15+
filename?: string | undefined
1416
dirname?: string | undefined
1517
globals?: Context | undefined
1618
useCurrentGlobal?: boolean | undefined
1719
}
1820

1921
export const requireFromString = (
2022
code: string,
21-
{ dirname = getCallerDirname(), globals = {}, useCurrentGlobal = false }: Options | undefined = {}
23+
{
24+
filename = `${nanoid()}.js`,
25+
dirname = getCallerDirname(),
26+
globals = {},
27+
useCurrentGlobal = false
28+
}: Options | undefined = {}
2229
): any => {
23-
const moduleFilename = join(dirname, `${nanoid()}.js`)
30+
const moduleFilename = ensurePath(getModuleFilename(dirname, filename))
2431
const mainModule = isInESModuleScope() ? undefined : require.main
2532
const contextModule = new Module(moduleFilename, mainModule)
2633

src/utils.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { dirname, isAbsolute, resolve } from 'path'
1+
import { dirname, isAbsolute, resolve, sep } from 'path'
22
import { URL, fileURLToPath, pathToFileURL } from 'url'
33
import vm, { Context } from 'vm'
44

@@ -13,16 +13,15 @@ export const isInESModuleScope = (): boolean => {
1313
// @ts-expect-error: experimental
1414
export const isVMModuleAvailable = (): boolean => vm.Module !== undefined
1515

16-
const FILE_URL_SCHEME = 'file:'
16+
const FILE_URL_PROTOCOL = 'file:'
1717

18-
const isFileURL = (value: string): boolean => value.startsWith(FILE_URL_SCHEME)
18+
const isFileURL = (value: string): boolean => value.startsWith(FILE_URL_PROTOCOL)
1919

20-
// correct url using `URL` API,
21-
// because `path.join` transforms `file:///home` to `file:/home`
22-
export const pathToFileURLString = (value: string): string => {
23-
const url = isFileURL(value) ? new URL(value) : pathToFileURL(value)
24-
return url.toString()
25-
}
20+
export const ensureFileURL = (value: string): string =>
21+
isFileURL(value) ? value : pathToFileURL(value).toString()
22+
23+
export const ensurePath = (value: string): string =>
24+
isFileURL(value) ? fileURLToPath(value) : value
2625

2726
const internalFunctionNames: readonly string[] = [
2827
'getCallerDirname',
@@ -43,7 +42,25 @@ export const getCallerDirname = (): string => {
4342
Error.prepareStackTrace = __prepareStackTrace
4443
const caller = callSites[0]
4544
const callerFilename = caller.getFileName() ?? process.argv[1]
46-
return dirname(isFileURL(callerFilename) ? fileURLToPath(callerFilename) : callerFilename)
45+
return dirname(ensurePath(callerFilename))
46+
}
47+
48+
const normalizeDirname = (dirname: string): string => {
49+
const separater = isFileURL(dirname) ? '/' : sep
50+
return dirname.endsWith(separater) ? dirname : `${dirname}${separater}`
51+
}
52+
53+
export const getModuleFilename = (dirname: string, filename: string): string => {
54+
if (isInESModuleScope()) {
55+
if (isFileURL(filename)) {
56+
return filename
57+
} else {
58+
const normalizedDirname = normalizeDirname(dirname)
59+
return new URL(filename, ensureFileURL(normalizedDirname)).toString()
60+
}
61+
} else {
62+
return resolve(ensurePath(dirname), ensurePath(filename))
63+
}
4764
}
4865

4966
const forEachPropertyKey = (
@@ -106,6 +123,6 @@ export const resolveModuleSpecifier = (specifier: string, dirname: string): stri
106123
return specifier
107124
}
108125
return specifier.startsWith('.') || isAbsolute(specifier)
109-
? resolve(dirname, specifier)
126+
? resolve(ensurePath(dirname), specifier)
110127
: specifier
111128
}

0 commit comments

Comments
 (0)