forked from wayfair/bagel
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
299 lines (254 loc) · 8.5 KB
/
index.js
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
// @flow
import path from 'path';
import fs from 'fs';
import defaultResolver from './default_resolver';
import isBuiltinModule from './is_builtin_module';
import wrapRequireWithInterceptors from './wrap_require_with_interceptors';
import lruCache from 'lru-cache';
import vm from 'vm';
type Resolver = (
dependencyID: string,
from: string,
requestContext: {}
) => string | null;
type Interceptor = ({
moduleID: string,
requestContext: {[string]: any},
next: string => any
}) => any;
type Transformer = ({path: string, source: string}) => {
errors: Array<string>,
transformedSource: string
};
type LoadModule = (
moduleID: string,
loadModuleFrom: string,
requestContext: {[string]: any}
) => any;
type GenerateModuleCacheKey = ({
moduleID: string,
requestContext: Object,
pathToSourceFile: string
}) => string | null;
// Module resolution is expensive. If files live in node_modules, we can cache
// them for the lifetime of the application. Caching other modules may be trickier
// since they may be managed by an application deployment process.
const nodeModuleResolverCache = {};
// The 'resolve' function loops through resolvers provided, falling back
// on default node resolution if none of the provided resolvers are able
// to resolve the module.
const makeResolve = ({resolvers, requestContext}) => (dependencyID, from) => {
let pathToModule;
const resolveNodeModule = (id, from) => {
if (!nodeModuleResolverCache[id]) {
nodeModuleResolverCache[id] = defaultResolver(id, {
basedir: path.dirname(from)
});
}
return nodeModuleResolverCache[id];
};
for (const resolver of [...resolvers, resolveNodeModule]) {
// $FlowFixMe variable number of arguments for resolver
const resolved = resolver(dependencyID, from, requestContext);
if (resolved) {
pathToModule = resolved;
break;
}
}
return pathToModule;
};
// Get the full filename based on a module name and path to parent
const getFilename = ({resolve, moduleName, pathToParent}) => {
let filename;
try {
filename = resolve(moduleName, pathToParent);
} catch (e) {
e.__rootError = true;
if (e.message) {
e.message = `Could not resolve module: ${moduleName}, imported by ${pathToParent}
${e.message}
`;
}
throw e;
}
if (!filename) {
throw new Error(
`Could not resolve module: ${moduleName}, imported by ${pathToParent}`
);
}
return filename;
};
// Transform the initial module source code with each of the transformer
// functions.
const getModuleSource = ({filename, sourceCodeTransformers}) => {
return sourceCodeTransformers.reduce((source, transformer) => {
const {errors, transformedSource} = transformer({
path: filename || '',
source
});
if (errors.length) {
throw new Error(`[LoadModule] Transform error(s):\n${errors.join('\n')}`);
}
return transformedSource;
}, fs.readFileSync(filename, 'utf-8'));
};
// Compile and cache module wrappers
const makeLoadModuleExports = ({
filename,
cacheKey,
moduleWrapperCache,
finalizeSource
}) => {
let loadModuleExports;
if (cacheKey) {
loadModuleExports = moduleWrapperCache.get(cacheKey);
}
if (!loadModuleExports) {
// @NOTE we are not sandboxing, and share globals.
// however we cache the wrapper, not the exports, so each module is unique per request.
loadModuleExports = vm.runInThisContext(finalizeSource(), {
filename,
cacheKey
});
if (cacheKey) {
moduleWrapperCache.set(cacheKey, loadModuleExports);
}
}
return loadModuleExports;
};
const createLoadModule = (options: {
resolvers?: Array<Resolver>,
interceptors?: Array<Interceptor>,
sourceCodeTransformers?: Array<Transformer>,
wrapModule?: string => string,
generateModuleCacheKey?: GenerateModuleCacheKey,
useResolverCache?: boolean
}) => {
const {resolvers, interceptors, sourceCodeTransformers, useResolverCache} = {
resolvers: [],
interceptors: [],
sourceCodeTransformers: [],
// If you pass true for this option, the resolved paths for imports will be cached.
// Leave this off if you need to handle relative imports.
useResolverCache: false,
...options
};
const moduleWrapperCache = lruCache({max: 10000});
// Pass in your own cache key generator or default to filepath
const generateModuleCacheKey =
options.generateModuleCacheKey ||
(({pathToSourceFile}) => pathToSourceFile);
// Pass in your own wrapper function if you like. This is one way (though probably not the best way) to insert additional functionality
// in to your module. For example, the 'define' function that an AMD module relies on.
const wrapModule =
options.wrapModule ||
(source => `(function (exports, require, module, __filename, __dirname) {
${source}
})`);
/**
* This is the actual loadModule function
*/
const moduleHandler: LoadModule = (
moduleID,
loadModuleFrom,
requestContext
) => {
// Store all the module exports here in order to avoid having more than one
// instance of a given module in the dependency graph.
const moduleRegistry = {};
const resolve = makeResolve({resolvers, requestContext});
// Inside a single dependency graph, we may be requiring the same module many times. Cache
// the location of the module
const moduleResolutionCache = {};
// The 'require' function we provide to each module has to be unique for that module,
// because it needs to know where the module source file lives in order to do relative imports correctly.
// makeRequire creates this function.
const makeRequire = pathToParent => {
const customRequire = moduleName => {
if (!moduleName) {
throw new Error('moduleName is not defined');
}
if (isBuiltinModule(moduleName)) {
// $FlowFixMe flow doesn't like non-string-literals passed to require, but I'm not sure how else to do this.
return require(moduleName);
}
let filename;
if (useResolverCache) {
if (!moduleResolutionCache[moduleName]) {
moduleResolutionCache[moduleName] = getFilename({
moduleName,
pathToParent,
resolve
});
}
filename = moduleResolutionCache[moduleName];
} else {
filename = getFilename({
moduleName,
pathToParent,
resolve
});
}
if (moduleRegistry[filename]) {
return moduleRegistry[filename].exports;
}
const cacheKey = generateModuleCacheKey({
moduleID: moduleName,
pathToSourceFile: filename,
requestContext
});
try {
const loadModuleExports = makeLoadModuleExports({
filename,
cacheKey,
moduleWrapperCache,
finalizeSource: () =>
wrapModule(getModuleSource({filename, sourceCodeTransformers}))
});
// these exports are mutated, reference from one variable for simplicity
moduleRegistry[filename] = {exports: {}};
loadModuleExports(
moduleRegistry[filename].exports,
makeRequire(filename),
moduleRegistry[filename],
filename,
path.dirname(filename)
);
} catch (e) {
if (!e.__rootError) {
e.__rootError = true;
e.message = `Error loading module ${moduleName} from ${pathToParent}.
${e.message}`;
// Don't add to the error message if we're at the root of the
// dependency graph
} else if (moduleName !== moduleID) {
e.message = `Error loading dependencies of ${pathToParent}.
${e.message}`;
}
throw e;
}
return moduleRegistry[filename].exports;
};
customRequire.resolve = moduleID => resolve(moduleID, pathToParent);
// Give the interceptors a chance to intercept and/or modify the call to require
// It's conceivable that interceptors may want to know what a module's resolved filename is.
// We could modify the api to pass in the resolve function. I see an immediate use case for that
// now though.
return wrapRequireWithInterceptors(
customRequire,
requestContext,
interceptors
);
};
return makeRequire(loadModuleFrom)(moduleID);
};
return moduleHandler;
};
export default createLoadModule;
export type {
Resolver,
Interceptor,
Transformer,
LoadModule,
GenerateModuleCacheKey
};