NPM + Zurb Foundation + WebPack: Cannot resolve module 'foundation'

送分小仙女□ 提交于 2019-11-29 21:48:13

I have same problem, but I don't want have two .js files (vendor and app)!

For me, everything need be on a single file, so, I make this:

In webpack.conf.js, use externals (maybe have another way without external, but for me, this is sufficient):

externals: {
    jQuery: 'jQuery',
    foundation: 'Foundation'
},

create a file in your source folder (any name, like /libs/foundation.js):

// jQuery
var $ = require('jquery');
global.jQuery = $;

// if you want all features of foundation
require('./node_modules_folder/foundation-sites/dist/foundation.js');

// if you want only some features
// require('./node_modules/what-input/what-input');
// require('./node_modules/foundation-sites/js/foundation.core');
// require('./node_modules/foundation-sites/js/....');

export default Foundation;

now, you can use Foundation in any js using following syntax:

import Foundation from './libs/foundation';

I was able to do this with webpack by essentially doing an end-run around loading it as a module.

This is basically a hack though, Foundation really needs to update its JS to be loadable as a commonJS module.

The problem stems from Foundation's JS referencing dependencies in erratic ways from within nested IFFEs in the souce code. Sometimes jQuery is the local jQuery parameter, sometimes it's $, sometimes it's window.jQuery. It's really a mixed-bag. The combination of all the different mechanisms means there's no single shimming solution other than to just load the thing non-modularly.

Honestly it's pretty much amateur hour in there, but as of this writing they just released the thing last week, so hopefully it'll be fixed soon.

Anyhoo... to the hack:

I make a separate vendor bundle and load all the amateur-hour 3rd party npm libraries there because I just get tired of fighting with all the various shimming mechanisms necessary to wrap poorly-shipped open-source npm package code.

My vendor bundle is a separate entry point that I register with webpack, and it contains all the libraries that do not play nice as modules.

require('!!script!jquery/dist/jquery.min.js');

require('!!script!uglify!foundation-sites/js/foundation.core.js');
require('!!script!uglify!foundation-sites/js/foundation.accordion.js');
require('!!script!uglify!foundation-sites/js/foundation.util.keyboard.js');
require('!!script!uglify!foundation-sites/js/foundation.util.motion.js');
// etc.

Make sure you have script-loader installed

npm install script-loader -D

The !! means "Ignore all the other rules I already defined in the config". Using the script-loader tells webpack to load and execute the file in the window scope essentially the same as if you had just included a script tag on your page. (But it doesn't, obviously.)

You could get fancier and write your own resolve rules so that it checks just stuff in the foundation-library, but I didn't bother because I hope a library as pervasive as Foundation gets their act together in the near future so I can just delete this hack.

Also... in your main webpack configuration you will want to reference jQuery and any other global window variables loaded in this way as externals.

var webpackConfig = {
    entry: { // blah },
    output: { // blah },
    loaders: [ // blah ],
    externals: {
        jquery: "jQuery"
    }
};

I'll post my complete workaround based on Mason Houtz and pharmakon's great answers in case it helps someone, since I struggled with it a bit, learning Webpack in the process.

In my case I had an added complication, because other jQuery plugins were somehow working only inside their own module, while outside their properties were undefined. Apparently they were using a local, duplicate jQuery object.

Anyway, here's what you need to do:

  1. Install scripts-loader: npm install --save-dev script-loader

  2. In Webpack's config:

    • Add new entry, let's call it vendor. This will compile a new vendor.js whenever Webpack runs.

      entry: {
          ...,
          "vendor": [
              "!!script!jquery/dist/jquery.min.js",
              "!!script!foundation-sites/dist/foundation.min.js"
          ]
      },
      
    • Add jquery to externals. This makes sure any references to jquery inside your main JS will be replaced with a reference to global jQuery variable, which is made available by vendor.js above.

      entry : {
          // ...
      },
      externals: {
          jquery: "jQuery"
      }
      
  3. Make sure every module that uses jQuery imports it:

    var $ = require('jquery');
    

The externals config above will replace it with a reference to global jQuery variable, instead of re-importing duplicate jQuery "properly". Optionally you could make use of ProvidePlugin, which will automatically do the above whenever it encounters jQuery in a module, saving you a few keystrokes. If you want that, put the following in Webpack's config:

plugins: [
    // ...,
    new webpack.ProvidePlugin({
      '$': 'jquery', 
      jQuery: 'jquery'
    })
]
  1. Include the new vendor.js to your page, obviously before the main JS.

It's quite possible there's an easier or more elegant way to do this, but I just wanted a quick, working solution, until Foundation hopefully fixes the issue soon.

Just use the script-loader (npm i script-loader) and prefix your imports with a script!. Then it will be evaluated in the global scope.

To load all js files from foundation use this

import 'script!jquery'
import 'script!what-input'
import 'script!foundation-sites'

Like I do it in my entry point

You can check out my boilerplate project to try it out: https://github.com/timaschew/r3-foundation-boilerplate

While @roberto's answer looks great, I wanted to provide a simpler solution (in which it does not require any extra vendor/foundation files).

In your webpack config use this:

// this will force the export of the jQuery 'foundation' function, 
// which we'll use later on
loaders: [
  {
    test: /(foundation\.core)/,
    loader: 'exports?foundation=jQuery.fn.foundation'
  }
],

// this makes sure that every module can resolve define(['foundation']) calls
resolve: {
  extensions: ['', '.js'],
  alias: {
    foundation: 'foundation-sites/js/foundation.core'
  }
},

// this makes sure 'jQuery' is available to any jQuery plugin you might want 
// to load (including Foundation files) regardless of how they are written
plugins: [
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    'window.jQuery': 'jquery'
  })
]

And in your index.js:

// thanks to the ProvidePlugin we don't need to
// > import $ from 'jquery';

// required core foundation files
import { foundation } from 'foundation-sites/js/foundation.core';
import 'foundation-sites/js/foundation.util.mediaQuery';

/* import here any additional module */

// we need to attach the function we force-exported in the config
// above to the jQuery object in use in this file
$.fn.foundation = foundation;

// ready to go
$(document).ready(function() {
  $(document).foundation();
  …
});

NOTE #1 (thank you @mtyson)
You need to use the exports loader: $ npm install --save exports-loader or $ npm install --save-dev exports-loader

NOTE #2
Since jQuery is not global inside single modules (or for some other reason that is beyond my understanding), there might be problems relying on data- attributes for Foundation JS components. If that is the case, you can always use the pure javascript way as documented in Foundation docs.

Here is how I am using the hack. I put foundation and jquery in a separate entry point called vendor and loaded them with the script-loader. The only relevant bits are in the vendor entry point.

var path = require('path');
var webpack = require('webpack');
var hotMiddlewareScript = 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true';
var autoprefixer = require('autoprefixer');

module.exports = {
  name: 'main',

  devtool: 'eval',

  entry: {
    client: [
      path.resolve(__dirname, 'client', 'index.js'),
      hotMiddlewareScript
    ],
    vendor: [
      'font-awesome/css/font-awesome.css',
      'foundation-sites/dist/foundation-flex.css',
      '!!script!jquery/dist/jquery.min.js',
      '!!script!foundation-sites/dist/foundation.min.js',
    ]
  },

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    publicPath: '/dist/'
  },

  resolve: {
    modulesDirectories: ['node_modules', './client'],
    extensions: ['', '.js', '.jsx']
  },

  plugins: [
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin(),
    new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js'),
    new webpack.ProvidePlugin({'$': 'jquery', jQuery: 'jquery'})
  ],

  module: {
    loaders: [
      { test: /\.(js|jsx)$/, loaders: ['react-hot', 'babel-loader'], exclude: /node_modules/, include: path.resolve(__dirname, 'client') },
      { test: /\.scss$/, loader: "style!css!autoprefixer-loader?browsers=last 2 versions!sass" },
      { test: /\.css$/, loader: "style!css" },
      // { test: /\.(png|jpg|jpeg|gif)$/, loader: 'file-loader?name=images/[name].[ext]' },
      { test: /\.(webm|mp4|mov|m4v|ogg)$/, loader: 'file-loader?name=videos/[name].[ext]' },
      { test: /\.(eot|svg|ttf|woff|woff2)/, loader: 'file-loader?name=fonts/[name].[ext]' }
    ]
  }
};

It works fine for webpack if you can tell it to ignore the define test for the troublesome code below:

  if (typeof module !== 'undefined' && typeof module.exports !== 'undefined')
     module.exports = Reveal;
  if (typeof define === 'function')
     define(['foundation'], function() {
     return Reveal;
  });

The best way to do that is to use the imports-loader and set define to false.

require('foundation-sites/js/foundation.core.js');
require('foundation-sites/js/foundation.util.keyboard.js');
require('foundation-sites/js/foundation.util.box.js');
require('foundation-sites/js/foundation.util.triggers.js');
require('foundation-sites/js/foundation.util.mediaQuery.js');
require('foundation-sites/js/foundation.util.motion.js');
require('imports?define=>false!foundation-sites/js/foundation.reveal.js');
Dhaval Shekhada

If you carefully see the foundation-sites 6.2.0 modules, you will find the changes in path as following

  1. foundation-sites/dist/css/foundation.min.css
  2. foundation-sites/dist/js/foundation.min.js

So basically you have to changes in webpack confing files entry as following

module.exports =  {
         entry: ['script!jquery/dist/jquery.min.js',
        'script!foundation-sites/dist/js/foundation.min.js',
         './app/app.jsx'
       ]
}

and the entry for style should be like this

require('style!css!foundation-sites/dist/css/foundation.min.css');

Grisha_K

For myself, I used this solution:

I'm using the Laravel framework, so first I added the .webpackConfig (...) method to the webpack.mix.js file:

mix.js('resources/assets/js/app.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css')

// By default, files from the directory "/node_modules" are not processed by the Babel loader,
// so in the Webpack configuration,
// an exception was added for loading from the directory "/node_modules/foundation-sites/js".
.webpackConfig({
    module: {
        rules: [{
            // Section "// Babel Compilation." from "/node_modules/laravel-mix/src/builder/webpack-rules.js"
            test: /\.jsx?$/,
            // Thanks for the help with "exclude": http://qaru.site/questions/97960/import-a-module-from-nodemodules-with-babel-but-failed/624982#624982
            exclude(file) {
                if (file.startsWith(__dirname + '/node_modules/foundation-sites/js')) {
                    return false;
                }
                return file.startsWith(__dirname + '/node_modules');
            },
            use: [
                {
                    loader: 'babel-loader',
                    options: Config.babel()
                }
            ]
        }]
    }
});

Note: To install Foundation I used the package https://github.com/laravel-frontend-presets/zurb-foundation. And added the code to load Foundation into the /resources/assets/js/bootstrap.js file:

/**
 * We'll load jQuery and the Foundation jQuery plugin which provides support
 * for JavaScript based Foundation features such as modals and tabs. This
 * code may be modified to fit the specific needs of your application.
 */

try {
    window.$ = window.jQuery = require('jquery');

    require('foundation-sites/dist/js/foundation'); // 'foundation.min' can also be used if you like

    // The app plugins for the Foundation
    require('./plugins/entries/foundation');

} catch (e) {}

Secondly, I created the file /resources/assets/js/plugins/entries/foundation.js (The file is included in the code above // The app plugins for the Foundation.). In which I included my modules (example):

import { CropText } from '../cropText';
Foundation.plugin(CropText, 'CropText');

Third, I created two files for including the Foundation plugins:

1) /resources/assets/js/plugins/foundation.plugin.js

// By default, files from the directory "/node_modules" are not processed by the Babel loader,
// so in the Webpack configuration,
// an exception was added for loading from the directory "/node_modules/foundation-sites/js".
import { Plugin } from 'foundation-sites/js/foundation.plugin';

export {Plugin};

2) /resources/assets/js/plugins/foundation.util.mediaQuery.js

// By default, files from the directory "/node_modules" are not processed by the Babel loader,
// so in the Webpack configuration,
// an exception was added for loading from the directory "/node_modules/foundation-sites/js".
import { MediaQuery } from 'foundation-sites/js/foundation.util.mediaQuery';

export {MediaQuery};

In the fourth, I created a file for my plugin using the Foundation plugins template, which include 2 of the above files:

/resources/assets/js/plugins/cropText.js

'use strict';

import $ from 'jquery';
import { MediaQuery } from './foundation.util.mediaQuery';
import { Plugin } from './foundation.plugin';

/**
 * CropText plugin.
 * @plugin app.cropText
 */
class CropText extends Plugin {
    /**
     * Creates a new instance of CropText.
     * @class
     * @name CropText
     * @fires CropText#init
     * @param {Object} element - jQuery object to add the trigger to.
     * @param {Object} options - Overrides to the default plugin settings.
     */
    _setup(element, options = {}) {
        this.$element = element;
        this.options  = $.extend(true, {}, CropText.defaults, this.$element.data(), options);

        this.className = 'CropText'; // ie9 back compat
        this._init();
    }

    /**
     * Initializes the CropText plugin.
     * @private
     */
    _init() {
        MediaQuery._init();

        this.cropText();
    }

    /**
     * Crops the text.
     */
    cropText() {
        var size = +this.options.cropSize;

        $(this.$element).each(function(i, value) {
            var $valueText = $(value).text(),
                $valueHtml = $(value).html();

            if($valueText.length > size){
                $(value).html('<span>' + $valueText.slice(0, size).trim() + '</span>' + '...').wrapInner('<a></a>');

                var revealId = '#' + $(value).attr('data-open');

                if ($(revealId + ' .close-button').attr('data-close') != undefined) {
                    $(revealId + ' .close-button').before($valueHtml);
                } else {
                    $(revealId).append($valueHtml);
                }

                new Foundation.Reveal($(revealId), {
                    'data-overlay' : false
                });
            } else {
                $(value).removeAttr('data-open').removeAttr('tabindex');
            }
        });
    }
}

/**
 * Default settings for plugin
 */
CropText.defaults = {
    /**
     * The size of the cropped text.
     * @option
     * @type {number}
     * @default 255
     */
    cropSize: 255
};

export {CropText};

It's all. Next, I just need to include a standard JavaScript file in the HTML code of the page and initialize the Foundation (example):

/resources/views/layouts/app.blade.php

<script src=" {{ mix('/js/app.js') }} "></script>

<script>
    $(document).foundation();
</script>

P.S. Sorry for my English ;-), I used Google Translate.

This is the common place to land from web search with beginner questions related to the technologies mentioned in the question.

Here is my Zurb Foundation with SCSS and Webpack Example Project.

Feel free to clone it. It is licensed with the Unlicense.

Background

Although, the module compatibility issues are resolved with recent (> 6.4) versions of the Zurb Foundation, a basic setup may still seem like black magic for a beginner. I think there must be other and probably better examples like mine somewhere, but I just couldn't find them. I'm adding the fruits of my learning journey here in the hopes that it will make somebody's ride a bit less bumpy.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!