-
Notifications
You must be signed in to change notification settings - Fork 3.2k
/
sourceRelativeWebpackModules.ts
275 lines (212 loc) · 9.71 KB
/
sourceRelativeWebpackModules.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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
import Module from 'module'
import path from 'path'
import type { WebpackDevServerConfig } from '../devServer'
import debugFn from 'debug'
const debug = debugFn('cypress:webpack-dev-server:sourceRelativeWebpackModules')
export type ModuleClass = typeof Module & {
_load(id: string, parent: Module, isMain: boolean): any
_resolveFilename(request: string, parent: Module, isMain: boolean, options?: { paths: string[] }): string
_cache: Record<string, Module>
}
export interface PackageJson {
name: string
version: string
}
export interface SourcedDependency {
importPath: string
packageJson: PackageJson
}
export interface SourcedWebpack extends SourcedDependency {
module: Function
majorVersion: 4 | 5
}
export interface SourcedWebpackDevServer extends SourcedDependency {
module: {
new (...args: unknown[]): unknown
}
majorVersion: 3 | 4
}
export interface SourcedHtmlWebpackPlugin extends SourcedDependency {
module: unknown
majorVersion: 4 | 5
}
export interface SourceRelativeWebpackResult {
framework: SourcedDependency | null
webpack: SourcedWebpack
webpackDevServer: SourcedWebpackDevServer
htmlWebpackPlugin: SourcedHtmlWebpackPlugin
}
const originalModuleLoad = (Module as ModuleClass)._load
const originalModuleResolveFilename = (Module as ModuleClass)._resolveFilename
// We ship webpack@4 as part of '@cypress/webpack-batteries-included-preprocessor'. The path to this module
// serves as our fallback.
export const cypressWebpackPath = require.resolve('@cypress/webpack-batteries-included-preprocessor', {
paths: [__dirname],
})
// Source the users framework from the provided projectRoot. The framework, if available, will server
// as the resolve base for webpack dependency resolution.
export function sourceFramework (config: WebpackDevServerConfig): SourcedDependency | null {
debug('Framework: Attempting to source framework for %s', config.cypressConfig.projectRoot)
if (!config.framework) {
debug('Framework: No framework provided')
return null
}
const framework = { } as SourcedDependency
try {
const frameworkJsonPath = require.resolve(`${config.framework}/package.json`, {
paths: [config.cypressConfig.projectRoot],
})
const frameworkPathRoot = path.dirname(frameworkJsonPath)
// Want to make sure we're sourcing this from the user's code. Otherwise we can
// warn and tell them they don't have their dependencies installed
framework.importPath = frameworkPathRoot
framework.packageJson = require(frameworkJsonPath)
debug('Framework: Successfully sourced framework - %o', framework)
return framework
} catch (e) {
debug('Framework: Failed to source framework - %s', e)
// TODO
return null
}
}
// Source the webpack module from the provided framework or projectRoot. We override the module resolution
// so that other packages that import webpack resolve to the version we found.
// If none is found, we fallback to the bundled version in '@cypress/webpack-batteries-included-preprocessor'.
export function sourceWebpack (config: WebpackDevServerConfig, framework: SourcedDependency | null): SourcedWebpack {
const searchRoot = framework?.importPath ?? config.cypressConfig.projectRoot
debug('Webpack: Attempting to source webpack from %s', searchRoot)
const webpack = { } as SourcedWebpack
let webpackJsonPath: string
try {
webpackJsonPath = require.resolve('webpack/package.json', {
paths: [searchRoot],
})
} catch (e) {
if ((e as {code?: string}).code !== 'MODULE_NOT_FOUND') {
debug('Webpack: Failed to source webpack - %s', e)
throw e
}
debug('Webpack: Falling back to bundled version')
webpackJsonPath = require.resolve('webpack/package.json', {
paths: [cypressWebpackPath],
})
}
webpack.importPath = path.dirname(webpackJsonPath)
webpack.packageJson = require(webpackJsonPath)
webpack.module = require(webpack.importPath)
webpack.majorVersion = getMajorVersion(webpack.packageJson, [4, 5])
debug('Webpack: Successfully sourced webpack - %o', webpack)
;(Module as ModuleClass)._load = function (request, parent, isMain) {
if (request === 'webpack' || request.startsWith('webpack/')) {
const resolvePath = require.resolve(request, {
paths: [webpack.importPath],
})
debug('Webpack: Module._load resolvePath - %s', resolvePath)
return originalModuleLoad(resolvePath, parent, isMain)
}
return originalModuleLoad(request, parent, isMain)
}
;(Module as ModuleClass)._resolveFilename = function (request, parent, isMain, options) {
if (request === 'webpack' || request.startsWith('webpack/') && !options?.paths) {
const resolveFilename = originalModuleResolveFilename(request, parent, isMain, {
paths: [webpack.importPath],
})
debug('Webpack: Module._resolveFilename resolveFilename - %s', resolveFilename)
return resolveFilename
}
return originalModuleResolveFilename(request, parent, isMain, options)
}
return webpack
}
// Source the webpack-dev-server module from the provided framework or projectRoot.
// If none is found, we fallback to the version bundled with this package.
export function sourceWebpackDevServer (config: WebpackDevServerConfig, framework?: SourcedDependency | null): SourcedWebpackDevServer {
const searchRoot = framework?.importPath ?? config.cypressConfig.projectRoot
debug('WebpackDevServer: Attempting to source webpack-dev-server from %s', searchRoot)
const webpackDevServer = { } as SourcedWebpackDevServer
let webpackDevServerJsonPath: string
try {
webpackDevServerJsonPath = require.resolve('webpack-dev-server/package.json', {
paths: [searchRoot],
})
} catch (e) {
if ((e as {code?: string}).code !== 'MODULE_NOT_FOUND') {
debug('WebpackDevServer: Failed to source webpack-dev-server - %s', e)
throw e
}
debug('WebpackDevServer: Falling back to bundled version')
webpackDevServerJsonPath = require.resolve('webpack-dev-server/package.json', {
paths: [__dirname],
})
}
webpackDevServer.importPath = path.dirname(webpackDevServerJsonPath)
webpackDevServer.packageJson = require(webpackDevServerJsonPath)
webpackDevServer.module = require(webpackDevServer.importPath)
webpackDevServer.majorVersion = getMajorVersion(webpackDevServer.packageJson, [3, 4])
debug('WebpackDevServer: Successfully sourced webpack-dev-server - %o', webpackDevServer)
return webpackDevServer
}
// Source the html-webpack-plugin module from the provided framework or projectRoot.
// If none is found, we fallback to the version bundled with this package dependent on the major version of webpack.
// We ship both v4 and v5 of 'html-webpack-plugin' by aliasing the package with the major version (check package.json).
export function sourceHtmlWebpackPlugin (config: WebpackDevServerConfig, framework: SourcedDependency | null, webpack: SourcedWebpack): SourcedHtmlWebpackPlugin {
const searchRoot = framework?.importPath ?? config.cypressConfig.projectRoot
debug('HtmlWebpackPlugin: Attempting to source html-webpack-plugin from %s', searchRoot)
const htmlWebpackPlugin = { } as SourcedHtmlWebpackPlugin
let htmlWebpackPluginJsonPath: string
try {
htmlWebpackPluginJsonPath = require.resolve('html-webpack-plugin/package.json', {
paths: [searchRoot],
})
htmlWebpackPlugin.packageJson = require(htmlWebpackPluginJsonPath)
// Check that they're not using v3 of html-webpack-plugin. Since we should be the only consumer of it,
// we shouldn't be concerned with using our own copy if they've shipped w/ an earlier version
htmlWebpackPlugin.majorVersion = getMajorVersion(htmlWebpackPlugin.packageJson, [4, 5])
} catch (e) {
const err = e as Error & {code?: string}
if (err.code !== 'MODULE_NOT_FOUND' && !err.message.includes('Unexpected major version')) {
debug('HtmlWebpackPlugin: Failed to source html-webpack-plugin - %s', e)
throw e
}
const htmlWebpack = `html-webpack-plugin-${webpack.majorVersion}`
debug('HtmlWebpackPlugin: Falling back to bundled version %s', htmlWebpack)
htmlWebpackPluginJsonPath = require.resolve(`${htmlWebpack}/package.json`, {
paths: [
__dirname,
],
})
}
htmlWebpackPlugin.importPath = path.dirname(htmlWebpackPluginJsonPath),
htmlWebpackPlugin.packageJson = require(htmlWebpackPluginJsonPath),
htmlWebpackPlugin.module = require(htmlWebpackPlugin.importPath),
htmlWebpackPlugin.majorVersion = getMajorVersion(htmlWebpackPlugin.packageJson, [4, 5])
debug('HtmlWebpackPlugin: Successfully sourced html-webpack-plugin - %o', htmlWebpackPlugin)
return htmlWebpackPlugin
}
// Most frameworks follow a similar path for sourcing webpack dependencies so this is a utility to handle all the sourcing.
export function sourceDefaultWebpackDependencies (config: WebpackDevServerConfig): SourceRelativeWebpackResult {
const framework = sourceFramework(config)
const webpack = sourceWebpack(config, framework)
const webpackDevServer = sourceWebpackDevServer(config, framework)
const htmlWebpackPlugin = sourceHtmlWebpackPlugin(config, framework, webpack)
return {
framework,
webpack,
webpackDevServer,
htmlWebpackPlugin,
}
}
export function getMajorVersion <T extends number> (json: PackageJson, acceptedVersions: T[]): T {
const major = Number(json.version.split('.')[0])
if (!acceptedVersions.includes(major as T)) {
throw new Error(
`Unexpected major version of ${json.name}. ` +
`Cypress webpack-dev-server works with ${json.name} versions ${acceptedVersions.join(', ')} - saw ${json.version}`,
)
}
return Number(major) as T
}
export function restoreLoadHook () {
(Module as ModuleClass)._load = originalModuleLoad;
(Module as ModuleClass)._resolveFilename = originalModuleResolveFilename
}