Shopify / slate

Slate is a toolkit for developing Shopify themes. It's designed to assist your workflow and speed up the process of developing, testing, and deploying themes.
https://shopify.github.io/slate
MIT License
1.28k stars 364 forks source link

Loading Webpack Plugins to expose jQuery to 3rd party modules #575

Open lmartins opened 6 years ago

lmartins commented 6 years ago

Problem

I am trying to use https://github.com/discolabs/cartjs as a CommonJS module, which is a library that depends on jQuery to be available on the window object.

One way Webpack allows to use this is via the ProvidePlugin https://webpack.js.org/plugins/provide-plugin/, so I wonder how can we achieve that using Slate's config file.

More Information

I've tried to expose a plugins array to the dev config, but that unfortunately didn't worked:


const path = require('path');

const alias = {
  jQuery: path.resolve('./node_modules/jquery'),
  jquery: path.resolve('./node_modules/jquery'),
  'lodash-es': path.resolve('./node_modules/lodash-es')
};

const plugins = [
  new webpack.ProvidePlugin({
    jQuery: 'jquery',
    $: 'jquery',
    jquery: 'jquery'
  })
];

module.exports = {
  slateCssVarLoader: {
    cssVarLoaderLiquidPath: ['src/snippets/css-variables.liquid']
  },
  slateTools: {
    extends: {
      dev: { resolve: { alias }, plugins: plugins },
      prod: { resolve: { alias } }
    }
  }
};

The only thing that worked was to create a jQuery global variable inside my module, which feels like a wrong approach to it.

Any tips on better solution to this?

xhubert commented 6 years ago

I am trying to do this. Because I got the message WARNING in entrypoint size limit... while slate building.

  1. Include jQuery as a 3rd by the externals of webpack in 'production' to reduce the size of bundle file. It can be done by the following code. Refer: webpack externals
// ./slate.config.js
const externals = {
  jquery: 'jQuery',
};

module.exports = {
  slateCssVarLoader: {
    cssVarLoaderLiquidPath: ['src/snippets/css-variables.liquid'],
  },
  slateTools: {
    extends: {
      dev: {
        resolve: {alias},
      },
      prod: {
        resolve: {alias},
        externals,
      },
    },
  },
};
<!-- ./src/layout/theme.liquid -->
<head>
...
<script 
      src="https://code.jquery.com/jquery-3.3.1.min.js"
      integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
      crossorigin="anonymous"></script>
{% include 'script-tags', layout: 'theme' %}
...
</head>
  1. Import the local npm package directly in development (not done yet) Usually...it just need to add a condition of env for the jquery <script> , like this:
    <!-- ./src/layout/theme.liquid -->
    <head>
    ...
    {% if theme.env == 'production' %}
    <script 
      src="https://code.jquery.com/jquery-3.3.1.min.js"
      integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
      crossorigin="anonymous"></script>
    {% endif %}
    {% include 'script-tags', layout: 'theme' %}
    ...
    </head>

    But I don't know how to get the current env in liquid. Need help... In the other hand, is it possible to be done by extending the scripts-tags?

et1421 commented 6 years ago

Same here, I'm trying to upload the translation file for jQuery validate inside theme.liquid : <script src="https://localhost:8080/messages_fr.min.js"></script>

And I'm getting Uncaught ReferenceError: jQuery is not defined error inside Chrome console.

jonathanmoore commented 6 years ago

I have been digging into this issue as well, and like @lmartins I would have thought adding the webpack.ProvidePlugin into slate.config.js would do the trick.

Looking at slate-tools' dev.js and prod.js files it appears that Slate's core.js plugins are applied first, then the mode's plugins and finally the user config plugins. My guess is that the webpack.ProvidePlugin would need to be loaded prior to the webpack.HotModuleReplacementPlugin.

Adding the ProviderPlugin directly to core.js does work, but that's basically injecting jQuery into everything.

// core.js
...
  const plugins = [
    new webpack.ContextReplacementPlugin(
      /__appsrc__/,
      replaceCtxRequest(paths.src),
    ),
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
    }),
  ];
...

Similar to the newer slateCssVarLoader object in slate.config.js, it might make sense to provide a way to pass along and object of ProviderPlugins.

It's also entirely possible that I'm missing something obvious since I'm newer to webpack.

jonathanmoore commented 6 years ago

Here is a much cleaner solution to set global jQuery or $ for legacy modules and typical jQuery plugins. Best of all Slate allows for this to be supported with the slate.config.js file.

The correct approach is outlined on Webpack's docs under Granular Shimming. https://webpack.js.org/guides/shimming/#granular-shimming

The webpack compiler can understand modules written as ES2015 modules, CommonJS or AMD. However, some third party libraries may expect global dependencies (e.g. $ for jQuery). The libraries might also create globals which need to be exported. These "broken modules" are one instance where shimming comes into play.

Here is an example where the imports-loader is used to add a global $ and jQuery variable requiring jquery to the top of the shopify-cartjs module.

  1. Add webpack imports loader plugin: yarn add imports-loader
  2. Update slate.config.js to set jQuery and $ for the shopify-cartjs module for both dev and prod
// slate.config.js

/* eslint-disable no-undef */

const path = require('path');

const alias = {
  jquery: path.resolve('./node_modules/jquery'),
  'lodash-es': path.resolve('./node_modules/lodash-es'),
};

module.exports = {
  slateCssVarLoader: {
    cssVarLoaderLiquidPath: ['src/snippets/css-variables.liquid'],
  },
  slateTools: {
    extends: {
      dev: {
        resolve: {alias},
        module: {
          rules: [
            {
              test: require.resolve('shopify-cartjs'),
              use: 'imports-loader?$=jquery,jQuery=jquery',
            },
          ],
        },
      },
      dev: {
        resolve: {alias},
        module: {
          rules: [
            {
              test: require.resolve('shopify-cartjs'),
              use: 'imports-loader?$=jquery,jQuery=jquery',
            },
          ],
        },
      },
    },
  },
};

Give it a try, and it should close out this issue.

@t-kelly Should we start updating https://github.com/Shopify/slate/wiki/Slate-Config with these examples? Or did you have something specific in mind for that page?

lmartins commented 6 years ago

@jonathanmoore sorry I've missed the notification on your comment. This does help a lot, thank you! :)

patrickbjohnson commented 6 years ago

Oddly enough this isn't working for me in a pretty fresh slate build.

Also using it to get CartJS to play nicely with Slate.

Here's my slate.config.js file:

/* eslint-disable no-undef */

const path = require('path');

const alias = {
  jquery: path.resolve('./node_modules/jquery'),
  'lodash-es': path.resolve('./node_modules/lodash-es'),
};

module.exports = {
  slateCssVarLoader: {
    cssVarLoaderLiquidPath: ['src/snippets/css-variables.liquid'],
  },
  slateTools: {
    extends: {
      dev: {
        resolve: {alias},
        module: {
          rules: [
            {
              test: require.resolve('shopify-cartjs'),
              use: 'imports-loader?$=jquery,jQuery=jquery',
            },
          ],
        },
      },
      dev: {
        resolve: {alias},
        module: {
          rules: [
            {
              test: require.resolve('shopify-cartjs'),
              use: 'imports-loader?$=jquery,jQuery=jquery',
            },
          ],
        },
      },
    },
  },
};

Here's my package.json:

{
  "name": "soma-water",
  "version": "1.0.0-alpha.1",
  "private": true,
  "author": "Shopify Inc.",
  "description": "An opinionated, Slate compatible, starting point for developing Shopify themes.",
  "keywords": [
    "shopify",
    "theme"
  ],
  "bugs": "https://github.com/Shopify/starter-theme/issues",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/Shopify/starter-theme.git"
  },
  "devDependencies": {
    "@shopify/slate-tools": ">=1.0.0-beta.1",
    "babel-preset-shopify": "^16.2.0",
    "eslint-plugin-shopify": "^19.0.0",
    "stylelint-config-prettier": "^3.0.4",
    "stylelint-config-shopify": "^5.0.0"
  },
  "dependencies": {
    "@shopify/theme-a11y": "^1.0.0-alpha.3",
    "@shopify/theme-cart": "^1.0.0-alpha.3",
    "@shopify/theme-currency": "^1.0.0-alpha.3",
    "@shopify/theme-images": "^1.0.0-alpha.3",
    "@shopify/theme-rte": "^1.0.0-alpha.3",
    "@shopify/theme-sections": "^1.0.0-alpha.4",
    "@shopify/theme-variants": "^1.0.0-alpha.5",
    "imports-loader": "^0.8.0",
    "jquery": "^3.2.1",
    "lazysizes": "^4.0.2",
    "lodash-es": "^4.17.4",
    "normalize.css": "^7.0.0",
    "shopify-cartjs": "^0.4.1"
  },
  "scripts": {
    "start": "slate-tools start",
    "watch": "slate-tools start --skipFirstDeploy",
    "build": "slate-tools build",
    "deploy": "slate-tools build && slate-tools deploy",
    "zip": "slate-tools build && slate-tools zip",
    "lint": "slate-tools lint",
    "format": "slate-tools format"
  }
}

I'm initializing CartJS at the bottom of my theme.liquid layout file.

<!doctype html>
    <!--[if IE 9]> <html class="ie9 no-js supports-no-cookies" lang="{{ shop.locale }}"> <![endif]-->
    <!--[if (gt IE 9)|!(IE)]><!--> <html class="no-js supports-no-cookies" lang="{{ shop.locale }}"> <!--<![endif]-->
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <meta name="theme-color" content="{{ settings.color_accent }}">
        <link rel="canonical" href="{{ canonical_url }}">

        {%- if settings.favicon != blank -%}
            <link rel="shortcut icon" href="{{ settings.favicon | img_url: '32x32' }}" type="image/png">
        {%- endif -%}

        {%- capture seo_title -%}
            {{ page_title }}
            {%- if current_tags -%}
            {%- assign meta_tags = current_tags | join: ', ' -%} &ndash; {{ 'general.meta.tags' | t: tags: meta_tags -}}
            {%- endif -%}
            {%- if current_page != 1 -%}
            &ndash; {{ 'general.meta.page' | t: page: current_page }}
            {%- endif -%}
            {%- assign escaped_page_title = page_title | escape -%}
            {%- unless escaped_page_title contains shop.name -%}
            &ndash; {{ shop.name }}
            {%- endunless -%}
        {%- endcapture -%}
        <title>{{ seo_title | strip }}</title>

        {%- if page_description -%}
            <meta name="description" content="{{ page_description | escape }}">
        {%- endif -%}

        {% include 'social-meta-tags' %}
        {% include 'style-tags' %}
        {% include 'css-variables' %}

        <script>
            document.documentElement.className = document.documentElement.className.replace('no-js', '');

            window.theme = {
            strings: {
                addToCart: {{ 'products.product.add_to_cart' | t | json }},
                soldOut: {{ 'products.product.sold_out' | t | json }},
                unavailable: {{ 'products.product.unavailable' | t | json }}
            },
            moneyFormat: {{ shop.money_format | json }}
            };
        </script>

        {% include 'script-tags', layout: 'theme' %}

        {{ content_for_header }}
    </head>

    <body id="{{ page_title | handle }}" class="template-{{ template.name | handle }}">

        <a class="in-page-link visually-hidden skip-link" href="#MainContent">{{ 'general.accessibility.skip_to_content' | t }}</a>

        {% section 'header' %}

        <main role="main" id="MainContent">
            {{ content_for_layout }}
        </main>

        {% section 'footer' %}

        {{ 'option_selection.js' | shopify_asset_url | script_tag }}

        <script type="text/javascript">
            jQuery(function() {
                CartJS.init({{ cart | json }});
            });
        </script>

    </body>
</html>

Hate to make this seem like a trouble-shooting type question but after literally copy/pasting and installing imports-loader I'm at a loss on how to get Shopify to play nice.

calebcurtis8 commented 6 years ago

@patrickbjohnson I'm at the same point as you, did you figure it out?

diegopelaezie commented 6 years ago

Hey guys, has anyone found the solution for this? I'm trying to import owl.carousel and I get similar errors, I tried @jonathanmoore solution with no luck.

This is what I have in my script: assets/scripts/components/collection_build.js

import $ from 'jquery';
import {formatMoney} from '@shopify/theme-currency';
import 'owl.carousel';

And this is my slate.config.js:

/* eslint-disable no-undef */

const path = require('path');

const alias = {
  jquery: path.resolve('./node_modules/jquery'),
  'lodash-es': path.resolve('./node_modules/lodash-es'),
};

module.exports = {
  slateCssVarLoader: {
    cssVarLoaderLiquidPath: ['src/snippets/css-variables.liquid'],
  },
  slateTools: {
    extends: {
      dev: {
        resolve: {alias},
        module: {
          rules: [
            {
              test: require.resolve('./node_modules/owl.carousel'),
              use: 'imports-loader?$=jquery,jQuery=jquery',
            },
          ],
        },
      },
      prod: {
        resolve: {alias},
        module: {
          rules: [
            {
              test: require.resolve('./node_modules/owl.carousel'),
              use: 'imports-loader?$=jquery,jQuery=jquery',
            },
          ],
        },
      },
    },
  },
};

And I get this error:

Uncaught TypeError: Cannot read property 'fn' of undefined
    at eval (webpack-internal:///../node_modules/owl.carousel/dist/owl.carousel.js:1718)
    at eval (webpack-internal:///../node_modules/owl.carousel/dist/owl.carousel.js:1755)
    at Object.../node_modules/owl.carousel/dist/owl.carousel.js (template.collection.js:1289)
    at __webpack_require__ (template.collection.js:712)
    at fn (template.collection.js:95)
    at eval (webpack-internal:///./assets/scripts/components/collection_build.js:9)
    at Object../assets/scripts/components/collection_build.js (template.collection.js:1471)
    at __webpack_require__ (template.collection.js:712)
    at fn (template.collection.js:95)
    at eval (webpack-internal:///./assets/scripts/templates/collection.js:7)
diegopelaezie commented 6 years ago

For anyone having the same issue as me with adding owl.carousel, I found that @jonathanmoore have found a way to do it, here is how he solved the issue.

Instead of using import:

import $ from 'jquery';
import {formatMoney} from '@shopify/theme-currency';
import 'owl.carousel';

You need to do this:

import $ from 'jquery';

window.jQuery = $;
window.$ = $;
require('owl.carousel');

Check his example here: https://github.com/jonathanmoore/starter-theme/pull/2/files

This is also mentioned on issue #605