Skip to content

Commit

Permalink
fix: handle resolve optional peer deps (#9321)
Browse files Browse the repository at this point in the history
  • Loading branch information
bluwy authored Aug 19, 2022
1 parent 2f468bb commit eec3886
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 2 deletions.
24 changes: 23 additions & 1 deletion packages/vite/src/node/optimizer/esbuildDepPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
moduleListContains,
normalizePath
} from '../utils'
import { browserExternalId } from '../plugins/resolve'
import { browserExternalId, optionalPeerDepId } from '../plugins/resolve'
import type { ExportsData } from '.'

const externalWithConversionNamespace =
Expand Down Expand Up @@ -93,6 +93,12 @@ export function esbuildDepPlugin(
namespace: 'browser-external'
}
}
if (resolved.startsWith(optionalPeerDepId)) {
return {
path: resolved,
namespace: 'optional-peer-dep'
}
}
if (ssr && isBuiltin(resolved)) {
return
}
Expand Down Expand Up @@ -279,6 +285,22 @@ module.exports = Object.create(new Proxy({}, {
}
)

build.onLoad(
{ filter: /.*/, namespace: 'optional-peer-dep' },
({ path }) => {
if (config.isProduction) {
return {
contents: 'module.exports = {}'
}
} else {
const [, peerDep, parentDep] = path.split(':')
return {
contents: `throw new Error(\`Could not resolve "${peerDep}" imported by "${parentDep}". Is it installed?\`)`
}
}
}
)

// yarn 2 pnp compat
if (isRunningWithYarnPnp) {
build.onResolve(
Expand Down
35 changes: 35 additions & 0 deletions packages/vite/src/node/plugins/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
isPossibleTsOutput,
isTsRequest,
isWindows,
lookupFile,
nestedResolveFrom,
normalizePath,
resolveFrom,
Expand All @@ -44,6 +45,8 @@ import { loadPackageData, resolvePackageData } from '../packages'
// special id for paths marked with browser: false
// https://github.com/defunctzombie/package-browser-field-spec#ignore-a-module
export const browserExternalId = '__vite-browser-external'
// special id for packages that are optional peer deps
export const optionalPeerDepId = '__vite-optional-peer-dep'

const isDebug = process.env.DEBUG
const debug = createDebugger('vite:resolve-details', {
Expand Down Expand Up @@ -365,6 +368,14 @@ export default new Proxy({}, {
})`
}
}
if (id.startsWith(optionalPeerDepId)) {
if (isProduction) {
return `export default {}`
} else {
const [, peerDep, parentDep] = id.split(':')
return `throw new Error(\`Could not resolve "${peerDep}" imported by "${parentDep}". Is it installed?\`)`
}
}
}
}
}
Expand Down Expand Up @@ -618,6 +629,30 @@ export function tryNodeResolve(
})!

if (!pkg) {
// if import can't be found, check if it's an optional peer dep.
// if so, we can resolve to a special id that errors only when imported.
if (
basedir !== root && // root has no peer dep
!isBuiltin(id) &&
!id.includes('\0') &&
bareImportRE.test(id)
) {
// find package.json with `name` as main
const mainPackageJson = lookupFile(basedir, ['package.json'], {
predicate: (content) => !!JSON.parse(content).name
})
if (mainPackageJson) {
const mainPkg = JSON.parse(mainPackageJson)
if (
mainPkg.peerDependencies?.[id] &&
mainPkg.peerDependenciesMeta?.[id]?.optional
) {
return {
id: `${optionalPeerDepId}:${id}:${mainPkg.name}`
}
}
}
}
return
}

Expand Down
8 changes: 7 additions & 1 deletion packages/vite/src/node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ export function isDefined<T>(value: T | undefined | null): value is T {
interface LookupFileOptions {
pathOnly?: boolean
rootDir?: string
predicate?: (file: string) => boolean
}

export function lookupFile(
Expand All @@ -400,7 +401,12 @@ export function lookupFile(
for (const format of formats) {
const fullPath = path.join(dir, format)
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
return options?.pathOnly ? fullPath : fs.readFileSync(fullPath, 'utf-8')
const result = options?.pathOnly
? fullPath
: fs.readFileSync(fullPath, 'utf-8')
if (!options?.predicate || options.predicate(result)) {
return result
}
}
}
const parentDir = path.dirname(dir)
Expand Down
13 changes: 13 additions & 0 deletions playground/optimize-deps/__tests__/optimize-deps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ test('dep with dynamic import', async () => {
)
})

test('dep with optional peer dep', async () => {
expect(await page.textContent('.dep-with-optional-peer-dep')).toMatch(
`[success]`
)
if (isServe) {
expect(browserErrors.map((error) => error.message)).toEqual(
expect.arrayContaining([
'Could not resolve "foobar" imported by "dep-with-optional-peer-dep". Is it installed?'
])
)
}
})

test('dep with css import', async () => {
expect(await getColor('.dep-linked-include')).toBe('red')
})
Expand Down
7 changes: 7 additions & 0 deletions playground/optimize-deps/dep-with-optional-peer-dep/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function callItself() {
return '[success]'
}

export async function callPeerDep() {
return await import('foobar')
}
15 changes: 15 additions & 0 deletions playground/optimize-deps/dep-with-optional-peer-dep/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "dep-with-optional-peer-dep",
"private": true,
"version": "0.0.0",
"main": "index.js",
"type": "module",
"peerDependencies": {
"foobar": "0.0.0"
},
"peerDependenciesMeta": {
"foobar": {
"optional": true
}
}
}
10 changes: 10 additions & 0 deletions playground/optimize-deps/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ <h2>
<h2>Import from dependency with dynamic import</h2>
<div class="dep-with-dynamic-import"></div>

<h2>Import from dependency with optional peer dep</h2>
<div class="dep-with-optional-peer-dep"></div>

<h2>Dep w/ special file format supported via plugins</h2>
<div class="plugin"></div>

Expand Down Expand Up @@ -152,6 +155,13 @@ <h2>Flatten Id</h2>
text('.reused-variable-names', reusedName)
</script>

<script type="module">
import { callItself, callPeerDep } from 'dep-with-optional-peer-dep'
text('.dep-with-optional-peer-dep', callItself())
// expect error as optional peer dep not installed
callPeerDep()
</script>

<script type="module">
// should error on builtin modules (named import)
// no node: protocol intentionally
Expand Down
1 change: 1 addition & 0 deletions playground/optimize-deps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dep-with-builtin-module-cjs": "file:./dep-with-builtin-module-cjs",
"dep-with-builtin-module-esm": "file:./dep-with-builtin-module-esm",
"dep-with-dynamic-import": "file:./dep-with-dynamic-import",
"dep-with-optional-peer-dep": "file:./dep-with-optional-peer-dep",
"added-in-entries": "file:./added-in-entries",
"lodash-es": "^4.17.21",
"nested-exclude": "file:./nested-exclude",
Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit eec3886

Please sign in to comment.