speedskater / babel-plugin-rewire

A babel plugin adding the ability to rewire module dependencies. This enables to mock modules for testing purposes.
843 stars 90 forks source link

Thanks for a great plugin! I'd suggest adding dynamic import syntax to the readme! :) #224

Open LouisStAmour opened 5 years ago

LouisStAmour commented 5 years ago

Not sure if this is working as intended or just a happy byproduct of Babel improvements, but I was able to use rewire APIs not just with top-level ES6 imports but also with dynamic imports using ES6 import() syntax. And with webpack, I can remove cached modules to force their re-evaluation. An example (assuming '../src/someFunc.js' contains a top-level const Q that it doesn't export, which in turn has a set named queue):

it('supports dynamic imports -- sweet', async () => {
    const exportedFunc = await import('../src/someFunc.js');
    const Q = exportedFunc.__get__('Q');
    expect(Q.queue.size).to.equal(0);
    Q.queue.add('test');
    expect(Q.queue.size).to.equal(1);
    // NOTE: The following line is required or webpack will return the same module instance in the next import...
    delete require.cache[require.resolve('../src/someFunc.js')];
    const exportedFunc = await import('../src/someFunc.js');
    const Q = exportedFunc.__get__('Q');
    expect(Q.queue.size).to.equal(0);
});

Very happy with how this is working out! :)

If it helps anybody, I'm using open-wc's Karma config with webpack config modified as follows (I've included the original open-wc Karma webpack config inline...):

{
    webpack: {
        watch: true,
        mode: 'development',
        devtool: 'inline-cheap-module-source-map',

        resolve: {
            mainFields: [
                // current leading de-facto standard - see https://github.com/rollup/rollup/wiki/pkg.module
                'module',
                // previous de-facto standard, superceded by `module`, but still in use by some packages
                'jsnext:main',
                // standard package.json fields
                'browser',
                'main',
            ],
        },

        module: {
            rules: [
                coverage && {
                    test: /\.js$/,
                    loader: require.resolve('istanbul-instrumenter-loader'),
                    enforce: 'post',
                    include: path.resolve('./'),
                    exclude: /node_modules|bower_components|\.(spec|test)\.js$/,
                    options: {
                        esModules: true,
                    },
                },

                legacy && {
                    test: /\.js$|\.ts$/,
                    use: {
                        loader: 'babel-loader',

                        options: {
                            plugins: [
                                require.resolve('@babel/plugin-syntax-dynamic-import'),
                                require.resolve('@babel/plugin-syntax-import-meta'),
                                // webpack does not support import.meta.url yet, so we rewrite them in babel
                                [require.resolve('babel-plugin-bundled-import-meta'), { importStyle: 'baseURI' }],
                            ].filter(_ => !!_),

                            presets: [[require.resolve('@babel/preset-env'), { targets: 'IE 11' }]],
                        },
                    },
                },

                !legacy && {
                    test: /\.js$/,
                    loader: require.resolve('@open-wc/webpack-import-meta-loader'),
                },
                {
                    test: /\.js$|\.ts$/,
                    exclude: /node_modules|bower_components|\.(spec|test)\.js$/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            plugins: [require.resolve('babel-plugin-rewire')].filter(_ => !!_),
                            sourceMaps: 'both',
                        },
                    },
                },
            ].filter(_ => !!_),
    },
}
LouisStAmour commented 5 years ago

To help mask the above webpack delete nastiness, I made a very simple wrapper using promises:

const importAndForget = filename => {
    return import(filename).finally(() => {
        delete require.cache[require.resolve(filename)];
    });
};

which simplifies the above example to:

it('supports dynamic imports -- sweet', async () => {
    importAndForget('../src/someFunc.js').then(module => {
        const Q = module.__get__('Q');
        expect(Q.queue.size).to.equal(0);
        Q.queue.add('test');
        expect(Q.queue.size).to.equal(1);
    });
    importAndForget('../src/someFunc.js').then(module => {
        const Q = module.__get__('Q');
        expect(Q.queue.size).to.equal(0);
    });
});
LouisStAmour commented 5 years ago

Had a hard time making the filenames dynamic while still keeping things await-compatible, so I ended up with the following function hard-coded:

function loadSomeFunc() {
    delete require.cache[require.resolve('../src/someFunc.js')];
    return import('../src/someFunc.js');
}

which can be used as follows:

    it('supports dynamic imports -- sweet', async () => {
        let module = await loadSomeFunc();
        let Q = module.__get__('Q');
        expect(Q.queue.size).to.equal(0);
        Q.queue.add('test');
        expect(Q.queue.size).to.equal(1);
        module = await loadSomeFunc();
        Q = module.__get__('Q');
        expect(Q.queue.size).to.equal(0);
    });