-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmod.js
304 lines (290 loc) · 9.9 KB
/
mod.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
300
301
302
303
304
import { ImportResolver } from "./src/ImportResolver.js";
/**
* @typedef OriginalModuleData
* @property {string} url
* @property {string} fullContent The full script content fetched from the original url without any modifications.
*/
/**
* @typedef {(() => string) | ((original: OriginalModuleData) => string)} ModuleImplementation
*/
/**
* @typedef ImporterOptions
* @property {"auto" | boolean} [generateCoverageMap] `"auto"` to look at command line flags for this option, or `true|false` to force enable or disable coverage map generation. Defaults to `"auto"`.
* @property {string} [coverageMapOutPath] When set, writes coverage map data to this directory.
* [more info about coverage maps](https://github.com/jespertheend/fake-imports#coverage)
* @property {string | URL | import("https://deno.land/x/[email protected]/mod.js").ImportMapData} [importMap] Use this to set the import map of the importer.
* You may only call this once, and only before making any imports.
* You can either pass a string that points to the import map (remote or on disk),
* or you can pass an import map object directly.
*
* ## Usage
* ```js
* const importer1 = new Importer(import.meta.url, {
* importMap: "../import-map.json", // import relatively to the current file
* });
*
* const importer2 = new Importer(import.meta.url, {
* importMap: "https://example.com/import-map.json", // import from a remote location
* });
*
* const importer3 = new Importer(import.meta.url, {
* importMap: {
* "imports": {
* "./foo.js": "https://example.com/foo.js",
* "./bar.js": "https://example.com/bar.js",
* },
* },
* });
* ```
* @property {boolean} [makeImportMapEntriesReal] When set to true (which is the default) all the entries
* in the import map will be marked as real with `exactMatch: true`. The
* assumption is made that the import map you have provided is the same import
* map as the one you are already using in your environment. In this case
* leaving this set as `true` should be fine. But if you haven't set an import
* map, you should probably set this to `false`.
* For more info about marking modules as real, see {@linkcode Importer.makeReal}.
*/
/**
* @typedef MakeRealOptions
* @property {boolean} [exactMatch] If set to true (default is false), only imports with this exact
* string are marked as real. Normally the the string you pass in should be relative to the `import.meta.url`
* passed to the `Importer` constructor, but with this option you can mark bare specifiers as real.
*
* This option is useful if you have an import map set up that the `Importer` doesn't know about, e.g.
* through Deno's `--import-map` argument, or using `<script type="importmap">` in
* browsers.
*
* For instance, say you have a file at `subdir/foo.js`:
*
* ```js
* import bar from "../bar.js";
* import * from "$std";
* ```
*
* If you want to make `bar.js` real, you can do so by providing a relative path from the root:
*
* ```js
* importer.makeReal("./bar.js");
* ```
*
* But `"$std"` doesn't have any way to point to it relative to the root, so you can use `exactMatch` in that case.
*
* ```js
* importer.makeReal("$std", {exactMatch: true});
* ```
*/
/**
* @typedef CoverageMapEntry
* @property {string} replacedUrl
* @property {string} originalUrl
* @property {import("./src/computeDiffOffsets.js").DiffOffsets} diffOffsets
*/
export class Importer {
#resolver;
/**
* Creates a new Importer instance. Creating multiple instances is supported
* and will cause scripts to get loaded in a somewhat isolated state. They'll
* be isolated in that modifications to a script imported from one importer
* do not affect other importers. However, if a script makes modifications
* to the global scope, this is not the case.
*
* For instance:
* ### foo.js
* ```js
* const someObject = { modified: false };
* export default someObject;
* ```
*
* ### modifyIt.js
* ```js
* import { someObject } from "foo.js";
* someObject.modified = true;
* ```
*
* ### run.js
* ```js
* const importer1 = new Importer(import.meta.url);
* await importer1.import("./modifyIt.js");
* const result1 = await importer1.import("./foo.js");
* console.log(result1.modified); // true
*
* // Creating a second importer will make seperate instances of all the scripts.
* const importer2 = new Importer(import.meta.url);
* const result2 = await importer2.import("./foo.js");
* console.log(result2.modified); // false
* ```
* @param {string | URL} importMeta
* @param {ImporterOptions} [options]
*/
constructor(importMeta, options = {}) {
/** @type {import("./src/ImportResolver.js").Environment} */
let env = "browser";
let deno = null;
if ("Deno" in globalThis) {
env = "deno";
deno = Deno;
}
/** @type {string[]} */
let args = [];
if (env === "deno") {
args = Deno.args;
}
this.#resolver = new ImportResolver(importMeta, options, {
env,
args,
deno,
});
}
/**
* Import a script. Similar to using the
* [dynamic `import()` statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports).
* Except with the faked modules applied.
* @template T
* @param {string} url
* @returns {Promise<T>}
*/
async import(url) {
return await this.#resolver.import(url);
}
/**
* Fakes a module. You can either pass a string or a function that returns a string.
*
* ## Usage
* If you just want to replace the entire content of a module, you can pass a string:
* ```js
* importer.fakeModule("./module.js", "export 'replaced'");
* ```
*
* If you want access to the original exports, faked modules can simply import themselves:
* ```js
* importer.fakeModule("./module.js", `
* import { original } from "./module.js";
* // Do something with original
* export { original };
* `);
* ```
*
* For more complex cases, you can pass a function that receives the original module data:
* ```js
* importer.fakeModule("./module.js", original => {
* return original.fullContent.replace("foo", "bar");
* });
* ```
* @param {string | URL} url should be relative to the `importMeta` argument provided in the {@link constructor}.
* @param {string | ModuleImplementation} moduleImplementation The code to replace the imported content with.
*/
fakeModule(url, moduleImplementation) {
this.#resolver.registerFakeModule(url, moduleImplementation);
}
/**
* Replaces a module with the content of another file.
* This has the added benefit that any imports within the new file will be resolved relative to its new path.
*
* This is almost the same as fetching content from a file and passing it to {@link fakeModule},
* except that imports of the new file are resolved relative to the new location.
*
* ## Usage
* Say you have the file `foo.js`:
*
* ```js
* export const foo = "foo";
* ```
*
* You can point `/foo.js` to another path using:
* ```js
* importer.redirectModule("/foo.js", "/long/path/to/fakeFoo.js");
* ```
*
* Now if `fakeFoo.js` contains an import like:
* ```js
* import {bar} from "./bar.js";
* ```
* then `bar` will be imported from `/long/path/to/bar.js`.
*
* If you want access to the original exports, redirected modules can simply import 'themselves' from the old url:
* ```js
* import {foo} from "../../../foo.js";
* ```
*
* @param {string | URL} url The old url you wish to replace.
* Should be relative to the `importMeta` argument provided in the {@link constructor}.
* @param {string | URL} newUrl The new url relative to the `importMeta` argument provided in the {@link constructor}.
*/
redirectModule(url, newUrl) {
this.#resolver.registerRedirectModule(url, newUrl);
}
/**
* Marks a specific module as needing to be imported by the real url, rather
* than a generated blob url. Though keep in mind that this will prevent
* `fakeModule` or `redirectModule` calls from having any effect on the module.
*
* This is useful if the module imports a lot of dependencies, as this
* prevents lots of blob urls from being created, which could potentially
* be very slow.
* This is also useful when your module creates instances that you want to
* test for using `instanceof`. For example, say you have a module that
* exports an instance of `Foo` like so:
*
* ```js
* import {Foo} from "./Foo.js";
* export const instance = new Foo();
* ```
*
* If you then import this module via a `new Importer()`, but import `Foo` via
* regular imports:
*
* ```js
* import {Foo} from "./Foo.js";
* const importer = new Importer(import.meta.url);
* const {instance} = await importer.import("./instance.js");
* ```
*
* and then test if `instance` is an instance of `Foo`:
*
* ```js
* assert(instance instanceof Foo);
* ```
*
* You'll get an error, because the two instances were actually created from
* a different class.
*
* To work around this, mark the module as real, so that internally no blob
* url is created for it:
*
* ```js
* importer.makeReal("./Foo.js");
* ```
*
* This way `instance.js` is still replaced with a blob url, but `Foo.js` is
* not.
* @param {string} url
* @param {MakeRealOptions} [options]
*/
makeReal(url, options) {
this.#resolver.makeReal(url, options);
}
/**
* Gets all coverage map data from all modules imported by this importer.
*
* [more info about coverage maps](https://github.com/jespertheend/fake-imports#coverage)
*/
getCoverageMap() {
return this.#resolver.getCoverageMap();
}
/**
* Fires when a new module is imported and provides coverage map data for
* this import.
*
* [more info about coverage maps](https://github.com/jespertheend/fake-imports#coverage)
* @param {(entry: CoverageMapEntry) => void} cb
*/
onCoverageMapEntryAdded(cb) {
this.#resolver.onCoverageMapEntryAdded(cb);
}
/**
* @param {(entry: CoverageMapEntry) => void} cb
*/
removeOnCoverageMapEntryAdded(cb) {
this.#resolver.removeOnCoverageMapEntryAdded(cb);
}
}