-
Notifications
You must be signed in to change notification settings - Fork 214
Expand file tree
/
Copy pathoverrides-resolver-loader.ts
More file actions
212 lines (182 loc) · 8.75 KB
/
overrides-resolver-loader.ts
File metadata and controls
212 lines (182 loc) · 8.75 KB
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
/*
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import {LoaderContext} from 'webpack'
import fse from 'fs-extra'
import os from 'os'
import path from 'path'
import resolve from 'resolve'
// Local Imports
import {buildCandidatePaths, getPackageName, SETUP_FILE_REGEX} from '../../shared/utils'
// Types
import type {ExtendedCompiler} from './types'
// Constants
const EXTENSION_PACKAGE_PREFIX = 'extension-'
const EXTENSION_PACKAGE_WORKSPACE = '@salesforce'
const IMPORT_REGEX = /import\s+(?:(?:[\w*\s{},]*)\s+from\s+)?['"](\..*?)['"]/g
const OVERRIDABLE_FILE_NAME = '.force_overrides'
const MONO_REPO_WORKSPACE_FOLDER = `${path.sep}packages${path.sep}`
const NODE_MODULES_FOLDER = `${path.sep}node_modules${path.sep}`
const REQUIRES_REGEX = /require\(['"](\..*?)['"]\)/g
const SRC = 'src'
// Cache for processed overrides
const OVERRIDABLE_CACHE = {
node: [] as string[],
web: [] as string[]
}
/**
* Webpack loader to override the resolution of a module based on the PWA-Kit applications
* extension configuration.
*
* This loader resolves a new resource path by evaluating possible overrides in other
* extensions, transpiling the file with the same loaders/plugins as the original file.
*
* @param {LoaderContext<any>} this - The Webpack loader context, which provides information
* about the module being processed and the current Webpack compiler.
*/
const OverrideResolverLoader = function (this: LoaderContext<any>) {
// Get the import path relative to the project base directory.
// NOTE: We intensionally exclude any path prefixes like "/" or "./" so that we can
// use `packageIterator` in the "resolve" function used later on.
const {resourcePath, _compiler} = this
const compiler = _compiler as ExtendedCompiler
const projectRelPath = resourcePath.split(`${SRC}${path.sep}`)[1].split('.')[0] // File path relative to the project directory without file extension
const projectPath = resourcePath.split(SRC)[0]
const options = this.getOptions()
// Get the package name
// NOTE: There is an opportunity to make this more performant as most of the time the file path will have
// the package name in it because it's in the node_modules folder and we can parse it out. But there are times,
// like when you use a mono-repo or local npm packages that you can't do this. So as a fallback you have to process
// the packageJSON file.
const packageName = getPackageName(projectPath, {filesystem: options.resolveOptions})
if (!packageName) {
console.warn('OVERRIDES-LOADER: Unable to determine import package name. Bailing...')
return resourcePath
}
// Lets use the compiler configuration to ensure we are resolving the correct file extensions.
const compilerOptions = this._compiler?.options
const extensions = compilerOptions?.resolve?.extensions || []
const basedir = options?.baseDir || process.cwd()
const applicationExtensions = compiler?.custom?.extensions || []
// Get the master list of all possible candidate paths based on your current extension configuration.
const paths = buildCandidatePaths(projectRelPath, packageName, {
canonicalSource: resourcePath,
projectDir: basedir,
extensionEntries: applicationExtensions
})
// Also include the base override path and the path from the extension doing the import.
const resolvedResourcePath = resolve.sync(projectRelPath, {
basedir,
extensions,
packageIterator: () => paths,
...options?.resolveOptions
})
// Tell Webpack to treat this new resource as a dependency of the original module in order to have the dependency
// transpiled with all the same loaders/plugins that the original file was.
this.addDependency(resolvedResourcePath)
// Use Webpack's `loadModule` function to load, process, and transpile the alternative module
const callback = this.async()
// Adjust the `basedir` dynamically for resolving relative imports in the new file
const newBasedir = path.dirname(resolvedResourcePath)
const convertRelativePaths = (match: string, relativePath: string) => {
let absolutePath = path.resolve(newBasedir, relativePath)
if (os.platform() === 'win32') {
absolutePath = absolutePath.replace(/\\/g, '\\\\')
}
return match.replace(relativePath, absolutePath)
}
// Load the replacement module adding a `noHMR=true` query so we can prevent the HMR plugin from trying
// to define its globals again.
this.loadModule(`${resolvedResourcePath}?noHMR=true`, (err, newSource) => {
if (err) return callback(err)
// NOTE: Convert all relative path imports to absolute path imports. This solves the problem of the wrong
// basedir being used when imports are resolved by webpack.
const adjustedSource = newSource
?.toString()
.replace(IMPORT_REGEX, convertRelativePaths) // Update relative imports
.replace(REQUIRES_REGEX, convertRelativePaths) // Update relative requires
// Return the loaded and transpiled content of the alternative module.
// NOTE: The third argument to the `callback` function is `sourceMap`. The fact that we aren't using
// that argument might be a point of debugging limitations in the future. Leaving this note here to tell
// future dev's this might be a place that needs to be adjusted.
callback(null, adjustedSource)
})
}
/**
*
* @param {*} source
* @returns
*/
const validateOverrideSource = (source: string, options: any = {}) => {
const {isMonoRepo = false, target = 'node', overridables = []} = options
const isExtensionFile = source.includes(`${path.sep}${EXTENSION_PACKAGE_PREFIX}`)
const isSetupFile = SETUP_FILE_REGEX.test(source)
const targetCache = OVERRIDABLE_CACHE[target as keyof typeof OVERRIDABLE_CACHE]
// Exit early if we have:
// 1. Processed this file already.
// 2. The file is not an extension file.
// 3. The file is an extension setup file.
if (targetCache.includes(source) || !isExtensionFile || isSetupFile) {
return false
}
// Because our webpack configuration is setup to resolve symlinks, we need to normalize the source path because
// the source path passed to the loaded is not representative of what you would see in a generated project (e.g.
// it doesn't resolve to being in the node_modules folder).
// NOTE:
// For now we are going to make the assumption that all our extension projects in our mono repo
// are part of the `@salesforce` namespace, this is pretty safe.
const normalizedSource = isMonoRepo
? `${EXTENSION_PACKAGE_WORKSPACE}${path.sep}${
source.split(MONO_REPO_WORKSPACE_FOLDER).pop() ?? ''
}`
: source.split(NODE_MODULES_FOLDER).pop()
// Check if the normalized source is in the list of overridables.
const hasOverride = overridables.includes(normalizedSource)
// If we have an override, add it to the cache so we don't process it again.
if (hasOverride) {
targetCache.push(source)
}
return hasOverride
}
/**
* Generates a Webpack rule for application extensibility, configuring the loader for
* handling application extensions based on the target (e.g., 'node' for server-side,
* 'react' for client-side).
*
* @param {Object} [options={}] - Options to customize the Webpack rule.
* @param {Object} [options.projectDir={}] - Loader-specific options.
* @param {string} [options.target=DEFAULT_TARGET] - The target environment, either 'node' or 'react'.
* @param {Object} [options.isMonoRepo] - Optional application configuration to pass to the loader.
*
* @returns {Object} A Webpack rule configuration object for handling application extensions.
*/
export const ruleForOverrideResolver = (options: any = {}) => {
const {projectDir, target, isMonoRepo} = options
let overridables: string[] = []
try {
overridables = fse
.readFileSync(path.join(projectDir, OVERRIDABLE_FILE_NAME), 'utf8')
.split(/\r?\n/)
.filter((line) => !line.startsWith('//'))
} catch (e) {
// If where is no .force_overrides file, we can safely return null.
return null
}
return {
test: (source: string) => {
return validateOverrideSource(source, {
isMonoRepo,
target,
overridables
})
},
use: {
loader: '@salesforce/pwa-kit-extension-sdk/configs/webpack/overrides-resolver-loader'
}
}
}
// Export the loader as the default export with proper typing
export default OverrideResolverLoader