This repository has been archived by the owner on May 22, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 35
/
plugin_dynamic_imports.ts
155 lines (134 loc) · 4.79 KB
/
plugin_dynamic_imports.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import { basename, join, relative } from 'path'
import type { Plugin } from '@netlify/esbuild'
import findUp from 'find-up'
import readPackageJson from 'read-package-json-fast'
import unixify from 'unixify'
import { parseExpression } from '../../parser/index.js'
type PackageCache = Map<string, Promise<string | undefined>>
// This plugin intercepts module imports using dynamic expressions and does a
// couple of things with them. First of all, it figures out whether the call
// is being made from within a Node module, and if so it adds the name of the
// module to `moduleNames`, so that we can warn the user of potential runtime
// issues. Secondly, it parses the dynamic expressions and tries to include in
// the bundle all the files that are possibly needed to make the import work at
// runtime. This is not always possible, but we do our best.
export const getDynamicImportsPlugin = ({
basePath,
includedPaths,
moduleNames,
processImports,
srcDir,
}: {
basePath?: string
includedPaths: Set<string>
moduleNames: Set<string>
processImports: boolean
srcDir: string
}): Plugin => ({
name: 'dynamic-imports',
setup(build) {
const cache: PackageCache = new Map()
// eslint-disable-next-line complexity
build.onDynamicImport({ filter: /.*/ }, async (args) => {
const { expression, resolveDir } = args
// Don't attempt to parse the expression if the base path isn't defined,
// since we won't be able to generate the globs for the included paths.
// Also don't parse the expression if we're not interested in processing
// the dynamic import expressions.
if (basePath && processImports) {
const { includedPathsGlob, type: expressionType } = parseExpression({ basePath, expression, resolveDir }) || {}
if (includedPathsGlob) {
// The parser has found a glob of paths that should be included in the
// bundle to make this import work, so we add it to `includedPaths`.
includedPaths.add(includedPathsGlob)
// Create the shim that will handle the import at runtime.
const contents = getShimContents({ expressionType, resolveDir, srcDir })
// This is the only branch where we actually solve a dynamic import.
// eslint-disable-next-line max-depth
if (contents) {
return {
contents,
}
}
}
}
// If we're here, it means we weren't able to solve the dynamic import.
// We add it to the list of modules with dynamic imports, which allows
// consumers like Netlify Build or CLI to advise users on how to proceed.
await registerModuleWithDynamicImports({ cache, moduleNames, resolveDir, srcDir })
})
},
})
const getPackageName = async ({ resolveDir, srcDir }: { resolveDir: string; srcDir: string }) => {
const packageJsonPath = await findUp(
async (directory) => {
// We stop traversing if we're about to leave the boundaries of the
// function directory or any node_modules directory.
if (directory === srcDir || basename(directory) === 'node_modules') {
return findUp.stop
}
const path = join(directory, 'package.json')
const hasPackageJson = await findUp.exists(path)
return hasPackageJson ? path : undefined
},
{ cwd: resolveDir },
)
if (packageJsonPath !== undefined) {
const { name } = await readPackageJson(packageJsonPath)
return name
}
}
const getPackageNameCached = ({
cache,
resolveDir,
srcDir,
}: {
cache: PackageCache
resolveDir: string
srcDir: string
}) => {
if (!cache.has(resolveDir)) {
cache.set(resolveDir, getPackageName({ resolveDir, srcDir }))
}
return cache.get(resolveDir)
}
const getShimContents = ({
expressionType,
resolveDir,
srcDir,
}: {
expressionType?: string
resolveDir: string
srcDir: string
}) => {
// The shim needs to modify the path of the import, since originally it was
// relative to wherever the importer sat in the file tree (i.e. anywhere in
// the user space or inside `node_modules`), but at runtime paths must be
// relative to the main bundle file, since esbuild will flatten everything
// into a single file.
const relativeResolveDir = relative(srcDir, resolveDir)
const requireArg = relativeResolveDir ? `\`./${unixify(relativeResolveDir)}/$\{args}\`` : 'args'
if (expressionType === 'require') {
return `module.exports = args => require(${requireArg})`
}
}
const registerModuleWithDynamicImports = async ({
cache,
moduleNames,
resolveDir,
srcDir,
}: {
cache: PackageCache
moduleNames: Set<string>
resolveDir: string
srcDir: string
}) => {
try {
const packageName = await getPackageNameCached({ cache, resolveDir, srcDir })
if (packageName !== undefined) {
moduleNames.add(packageName)
}
} catch {
// no-op
}
}