6

I'm trying to use Fuse in my TypeScript applications. I'm importing the module typings with import * as fuselib from 'fuse.js';. This compiles fine with tsc. The problem I'm running into is when I build the project using webpack --config config/webpack.prod.js --progress --profile --bail.

I receive error Cannot find module 'fuse.js'. The Fuse typings can be found here. Looking at my compiled JS, I can't find the word fuse.js, so I'm guessing Webpack is mangling the name. I tried ignoring keyword fuse.js in the UglifyJsPlugin, but that didn't help.

My Webpack configuration is pretty standard.

webpack.prod.js

var webpack = require('webpack');
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');

const ENV = process.env.NODE_ENV = process.env.ENV = 'production';

module.exports = webpackMerge(commonConfig, {
    devtool: 'source-map',

    output: {
        path: helpers.root('dist'),
        publicPath: '/',
        filename: '[name].[hash].js',
        chunkFilename: '[id].[hash].chunk.js'
    },

    htmlLoader: {
        minimize: false // workaround for ng2
    },

    plugins: [
        new webpack.NoErrorsPlugin(),
        new webpack.optimize.DedupePlugin(),
        new webpack.optimize.UglifyJsPlugin({ // https://github.com/angular/angular/issues/10618
            mangle: {
                keep_fnames: true,
                except: ['fuse.js']
            }
        }),
            new ExtractTextPlugin('[name].[hash].css'),
            new webpack.DefinePlugin({
                'process.env': {
                    'ENV': JSON.stringify(ENV)
                }
            })
    ]
});

webpack.common.js

var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var helpers = require('./helpers');

module.exports = {
    entry: {
        'polyfills': './src/polyfills.ts',
        'vendor': './src/vendor.ts',
        'app': './src/main.ts'
    },

    resolve: {
        extensions: ['', '.js', '.ts', '.tsx'],
        modulesDirectories: ['src', 'node_modules']
    },

    module: {
        loaders: [
        {
            test: /\.ts$/,
            loaders: ['awesome-typescript-loader', 'angular2-template-loader', 'angular2-router-loader']
        },
        {
            test: /\.html$/,
            loader: 'html'
        },
            {
                test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
                loader: 'file?name=assets/[name].[hash].[ext]'
            },
            {
                test: /\.css$/,
                exclude: helpers.root('src', 'app'),
                loader: ExtractTextPlugin.extract('style', 'css?sourceMap')
            },
                {
                    test: /\.css$/,
                    include: helpers.root('src', 'app'),
                    loader: 'raw'
                }
        ]
    },

    plugins: [
        // for materialize-css
        new webpack.ProvidePlugin({
            "window.jQuery": "jquery",
            "window._": "lodash",
            _: "lodash"
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: ['app', 'vendor', 'polyfills']
        }),
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        })
    ]
};

What am I missing in order to make Webpack see module fuse.js?

onetwothree
  • 672
  • 1
  • 10
  • 20

2 Answers2

7

Update

I wrote new declarations for the librarian question based on this answer. It should all just work fine with a recent version as they are shipped with the library.

Answer

OK, here is what is happening and why.

Firstly, Fuze/index.d.ts attempts to declare itself as both a global and as an ambient external module but does both of these incorrectly. This makes misuse, such as that which led your error almost inevitable.

It contains a module declaration that contains a class declaration, presumably with the intent of describing the shape of the module but the class is not exported.

declare module 'fuse.js' {
  class Fuze // missing one of: export, export =, export default
}

This means that I cannot import the module correctly and in fact there is a type error when trying to import a value and/or type from it.

Further down in Fuse/index.d.ts it declares its global

declare const Fuse;

Presumably, based on conventions and reading the comments in the actual JavaScript, this is meant to have the same shape as what is exported from the module. Unfortunately, it has type any which is neither the same type as the attempted module, because it isn't valid, nor the the type of the class Fuse which is trapped inside said module but not exported...

So why the error? You probably have one of the following somewhere in your program:

import 'fuse.js';

import Fuse from 'fuse.js';

import * as Fuse from 'fuse.js';

followed by some use of Fuse like

const myFuse = new Fuse();

This will cause an import for the runtime representation of Fuse fuse to be emitted by TypeScript, so that you can use the value imported from the module.

To fix the problem, you can use the global const Fuse and not import it anywhere. Unfortunately that is not what is intended. The author almost certainly meant to have the following content in Fuze/index.d.ts:

export = Fuse;

export as namespace Fuse;

declare class Fuse {
    constructor(list: any[], options?: Fuse.FuseOptions)
    search<T>(pattern: string): T[];
    search(pattern: string): any[];
}

declare namespace Fuse {
    export interface FuseOptions {
        id?: string;
        caseSensitive?: boolean;
        include?: string[];
        shouldSort?: boolean;
        searchFn?: any;
        sortFn?: (a: { score: number }, b: { score: number }) => number;
        getFn?: (obj: any, path: string) => any;
        keys?: string[] | { name: string; weight: number }[];
        verbose?: boolean;
        tokenize?: boolean;
        tokenSeparator?: RegExp;
        matchAllTokens?: boolean;
        location?: number;
        distance?: number;
        threshold?: number;
        maxPatternLength?: number;
        minMatchCharLength?: number;
        findAllMatches?: boolean;
    }
}

Which declares a class which is either available globally, for those not using modules, or via an import for those who are. You can use the above UMD style declaration to gain the typing experience the author intended. The one bundled with the library provides no type information and actually results in errors when used.

Consider sending a pull request to the maintainer with the fix.

Usage:

You can use this declaration in the following ways:

CommonJS, AMD, or UMD style

import Fuse = require('fuse.js');

const myFuseOptions: Fuse.FuseOptions = {
  caseSensitive: false
};
const myFuse = new Fuse([], myFuseOptions);

ES with CommonJS interop style

(when using "module": "system" or "allowSyntheticDefaltImports") with SystemJS, recent Webpacks, or if piping through Babel. As of typescript 2.7 you can use also use the new --esModuleInterop flag without any additional module tools or transpilers.

import Fuse from 'fuse.js';

const myFuseOptions: Fuse.FuseOptions = {
    caseSensitive: false
};
const myFuse = new Fuse([], myFuseOptions);

As of typescript 2.7, es module interop is now available in the language directly. That means you don't need to use Babel or SystemJS or webpack in order to write correct imports.

Aluan Haddad
  • 29,886
  • 8
  • 72
  • 84
  • See also https://github.com/krisk/Fuse/pull/124 with a workaround in its description. – Arjan Dec 24 '16 at 22:46
  • 1
    Using, for example, `import {Fuse, FuseOptions} from 'fuse.js';` or `import * as fuselib from 'fuse.js';` for the above `index.d.ts`, gets me `TS2497:Module '".../node_modules/fuse.js/index"' resolves to a non-module entity and cannot be imported using this construct.` Any idea...? – Arjan Dec 26 '16 at 15:34
  • Nice catch. I had a bug in my example I didn't export the interface to avoid polluting the global namespace with its typename. I have updated the example slightly and it now works as expected. Thank you. – Aluan Haddad Dec 27 '16 at 16:20
  • @Arjan note I also updated to explain how to use it correctly, your imports were not correct but you did find a problem with the declaration – Aluan Haddad Dec 27 '16 at 16:41
  • I thought I had this working but apparently not. I tried using your fix for `index.d.ts` as well tried importing using the 3 styles you suggested. While I'm not getting an error when compiling using the compiler, I am still getting an error when compiling with webpack. Same error about module `fuse.js` not being found. – onetwothree Jan 11 '17 at 14:14
0

The trick is to provide access to the global variable Fuse. It is done by webpack with the ProvidePlugin

add the following plugin to the webpack plugins array:

    ...
    new webpack.ProvidePlugin({
        "Fuse": "fuse.js"
    })
    ...
Kalle
  • 3,467
  • 1
  • 18
  • 22
  • How are you importing `fuse.js` in your code? I adding this line to my webpack config and the error persists. – onetwothree Jan 11 '17 at 14:48
  • @onetwothree: it is just: `import 'fuse.js';` With that line you tell typescript that this module is available – Kalle Jan 12 '17 at 18:02
  • That defeats the entire purpose of using modules – Aluan Haddad Sep 08 '17 at 00:40
  • Yes it is the antipattern of modules to have globals, but my quick fix answer shows a working solution for environments where you have globals. I think there are many legacy projects with many globally provided modules and it is sometimes easier to go with a quick fix – Kalle Sep 08 '17 at 06:18