npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

fake-imports

v0.10.0

Published

Fake, mock and stub modules.

Downloads

2

Readme

Fake Imports

Github ci Deno version npm license

This is a small module for Deno and browser environments that can be used for modifying the contents of imported modules. It can be used for stubbing or mocking modules in unit tests, among other things.

Take the following module for instance:

import {initConnection} from = "./database.js";

export async function readAndReturn() {
	const connection = initConnection();
	// Read something from the database
}

If you want to write tests for the readAndReturn() call, there is no way to prevent the initConnection() from being called. Ideally, a problem like this would prompt you to rewrite your code to use dependency injection, but this doesn't always make sense.

This module was created to solve this problem. By allowing you to replace the contents of an imported file, you can change its behavior, or prevent it from doing anything at all.

Usage

Let's take the previous example and assume that the file is called readAndReturn.js. Rather than importing this file directly, we'll create a new Importer() and load "./readAndReturn.js" dynamically:

import { Importer } from "https://deno.land/x/fake_imports/mod.js";
const importer = new Importer(import.meta.url);

const { readAndReturn } = await importer.import("./readAndReturn.js");

(note that in browser environments you can import from https://cdn.jsdelivr.net/npm/fake-imports@latest/dist/fake_imports.js)

This importer.import() method works pretty much the same as a regular dynamic import(), except that you get to modify files before you import them!

Right now the call to readAndReturn() would still try to connect to the database. Let's try to modify the contents of database.js before we import it:

importer.fakeModule(
	"./database.js",
	`
	export function initConnection() {}
`,
);

const { readAndReturn } = await importer.import("./readAndReturn.js");
readAndReturn(); // This should now work!

Modifying existing content

For more complex cases, it is also possible for faked modules to import themselves. This allows you to modify the state of the module right before it gets exported.

const importer = new Importer(import.meta.url);
importer.fakeModule(
	"./original.js",
	`
		import {someObject} from "./original.js";
		// Modify someObject here
		export {someObject};
`,
);

You can use a callback if you want complete control over what gets modified. The callback should return a string that contains the new content of the module. The callback receives a parameter with data about the original module, that way you can replace only specific words for example:

const importer = new Importer(import.meta.url);
importer.fakeModule("./original.js", (original) => {
	return original.fullContent.replace("foo", "bar");
});

Preventing modules from being faked

Faked modules come with some limitations, such as circular imports not being supported. On top of that, each imported file creates a new object URL, so it's best to not import very large module graphs.

Not to worry though! If there are modules that you wish to keep untouched, you can use makeReal() to prevent object URLs from being created for certain files.

const importer = new Importer(import.meta.url);
importer.makeReal("./Foo.js");

In the example above, if any module imports Foo.js, it won't be replaced by object URLs. As a result, any modules imported by Foo.js won't be replaced either.

Working with instanceof

Another reason you might want to make modules real is that classes imported using await importer.import() and actual import syntax are not the same. So if you are using instanceof it might not work as you would expect:

import { Foo as RealFoo } from "./Foo.js";

const importer = new Importer(import.meta.url);
const { Foo } = await importer.import("./Foo.js");

const foo = new Foo();
console.log(foo instanceof RealFoo); // false

You can fix this by using makeReal() as well. The above is a bit of a silly example because it doesn't really make sense to import the same file twice like that. But a more realistic scenario would be one where another file imports Foo.js:

import { Foo as RealFoo } from "./Foo.js";

const importer = new Importer(import.meta.url);
const { makeFooInstance } = await importer.import("./makeFooInstance.js");

const foo = makeFooInstance();
console.log(foo instanceof RealFoo); // true

Import maps

By default, an Importer is created without an import map. Even when you have already specified one using <script type="importmap"> or --import-map. So if you want to use it, you'll have to provide it again when instantiating the Importer. This can be done with importMap option:

const importer = new Importer(import.meta.url, {
	importMap: "./path/to/importMap.json",
});

You can provide a path to an import map, or provide an import map directly:

const importer = new Importer(import.meta.url, {
	importMap: {
		imports: {
			"lib": "./path/to/libary.js",
		},
	},
});

Import maps are assumed to be used for large libraries and generally things that don't need to be faked. So by default, all entries from the provided import map are marked as real. If you don't want this to happen you can set makeImportMapEntriesReal to false:

const importer = new Importer(import.meta.url, {
	importMap: "./path/to/importMap.json",
	makeImportMapEntriesReal: false,
});

Coverage

When using Fake Imports, object URLs are created for every file you import. For this reason, if you want to collect coverage for your tests, the coverage data generated by Deno will be incorrect. To work around this issue, you can generate a coverage map. This map contains info about changes that need to be made to the coverage data to make it accurate again.

Generating coverage maps via the command line

To generate coverage maps via the command line, you can use the --fi-coverage-map argument. To generate a coverage map for tests, for instance, you would run your tests like so:

deno test --coverage=./deno_coverage_dir -- --fi-coverage-map=./fi_coverage_dir

The extra -- is required to distinguish between arguments passed to Deno and arguments passed to your application.

Applying coverage maps

To apply the coverage map to your Deno coverage data you can run applyCoverageMap.js:

deno run --allow-read --allow-write https://deno.land/x/fake_imports/applyCoverageMap.js ./fi_coverage_dir ./deno_coverage_dir

This replaces the object URLs in the deno coverage data with the original URLs. and in case changes have been made to the contents of imported scripts via importer.fakeModule, the coverage positions will be offset in order to match the positions of the real script.

Generating coverage maps with JavaScript

Alternatively, a few methods are available for obtaining coverage map data with JavaScript:

const importer = new Importer(import.meta.url, {
	generateCoverageMap: true,
});
importer.onCoverageMapEntryAdded((entry) => {
	// do stuff with the entry here
});
// or after you have imported all your modules:
const coverageMap = importer.getCoverageMap();

How it works internally

When you import via importer.import(), the resource is first downloaded using fetch(). The content is then parsed and any imports from the file get the same treatment recursively. Then the content of all downloads is passed into URL.createObjectURL(), while replacing all import statements with the generated object URL. Finally, the root file is loaded using a regular await import(), causing all object URLs to get parsed and executed.

Caveats

  • Circular imports are not supported. Because of the way object URLs work, this is unfortunately not possible. The reason for this is that there is no way to modify the contents of an object URL after it has been created. Essentially, when a.js imports b.js, first an object URL is created for b.js, which is then inserted into the content of a.js. But if b.js imports a.js as well, a new object URL now needs to be generated for b.js, causing the two files to generate object URLs for each other forever.
  • Because fetch() is being used, the --allow-net permission is required. If you want to load scripts from the disk, --allow-read is also required.

Contributing

See CONTRIBUTING.md