jspm / jspm-cli

ES Module Package Manager

Home Page:https://jspm.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

0.17-beta.14 : rollup removes $__System from AMD builds

joeldenning opened this issue · comments

For some projects (seems like maybe just small single-file projects) If you jspm build my-project --format amd, then you get a built file that does not have the preamble and $__System.register calls. However, if you add --skip-rollup to the command, then the $__System.register calls come back. I'm guessing this might have something to do with rollup's tree shaking code removal? The reason this is problematic for me is that if an AMD build does not have the $__System.register calls in it, then default exported values from that module are not handled correctly when imported into another SystemJS module.

I created a repo to help describe this issue, please see https://github.com/joeldenning/jspm-skip-rollup-issue for a code example.

Thanks, I went ahead and added systemjs/builder@550e22c which will ensure Rollup treats the default export consistently with the sfx builds.

My only question though is that the output file https://github.com/joeldenning/jspm-skip-rollup-issue/blob/master/with-rollup.build.js is actually correct, and the fix will ensure that the sfx build does the same thing as this build, as we do collapse to the default export instead of explicitly requiring it for non esm module format outputs. The interop layer in the loader itself should then handle that that works correctly with named exports.

Perhaps you can clarify the exact interop issue?

I tried to make that sample repo I referenced above reproduce the interop problem but was unable to do so. It seems that the problem might be related to something specific that I'm doing in the closed-source project where the problem was first surfaced (it's a work project which is why it's closed-source).

What I'll do is I'll add you as a collaborator to the "fetcher" project temporarily so you can see the source code and the build process, and I'll also post here the built file that causes problems. What I notice is that weirdly the builder is declaring exports as an external dependency and that instead of returning the exported value (like I'd expect from an AMD build) that it is actually just putting things on the exports object.

The error happens when I do the following

import fetcher from 'fetcher';

fetcher('/some-url-to-get-json-from')

I get the error Uncaught TypeError: fetcher is not a function. When I debug the error, I find out that the fetcher object actually has a default property on it, so the interop is failing to handle the default export correctly.

// This is the output of running jspm build fetcher - cp-client-auth!sofe - rx - lodash sofe/fetcher.js --format amd --skip-source-maps


define(['exports', 'cp-client-auth!sofe', 'rx', 'lodash'], function (exports, auth, Rx, lodash) { 'use strict';

  auth = 'default' in auth ? auth['default'] : auth;
  Rx = 'default' in Rx ? Rx['default'] : Rx;

  var _toConsumableArray = (function (arr) {
    if (Array.isArray(arr)) {
      for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {
        arr2[i] = arr[i];
      }return arr2;
    } else {
      return Array.from(arr);
    }
  })

  var cache = {};

  function isCached(url) {
    return !!cache[url];
  }

  // Returns a subset of the cache that have a base URL matching that of the given URL, but with a different query string
  // These are items that are affected by a PUT/PATCH to the given URL but are not an exact match, & thus must handle their own updates (as opposed to using the PUT/PATCH response)
  function getAffectedCachedItems(url) {
    var baseUrl = url.split('?')[0];
    return lodash.transform(cache, function (result, value, key) {
        if (key !== url && key.split('?')[0] === baseUrl) {
            result[key] = value;
        }
    });
  }

  function updateCache(key, data) {
    cache[key].subject.onNext(data);
  }

  // Returns a Subject (both an Observable and Observer)
  function getWithSharedCache(url, cacheUntil) {
    var forceBust = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2];


    // url is in the cache
    if (cache[url]) {

        // Add new cache buster
        if (typeof cacheUntil === 'function') {
            cache[url].busters.push(cacheUntil);
        }

        if (_shouldBust(url) || forceBust) {
            // Make a new request and update the subject
            fetcher(url).then(function (resp) {
                if (resp.ok) resp.json(); // This ensures the observers are notified
            });
        }

        // Return what's in the cache
        return _getCache(url);

        // url is new to the cache
    } else {
            var subject = new Rx.ReplaySubject(1);
            _setCache(url, subject, cacheUntil);

            fetcher(url).then(function (resp) {
                if (resp.ok) resp.json(); // This ensures the observers are notified
            });

            return subject;
        }
  }

  function _getCache(key) {
    return cache[key].subject;
  }

  function _setCache(key, subject, cacheUntil) {
    if (!cache[key]) {
        cache[key] = {
            subject: subject,
            busters: typeof cacheUntil === 'function' ? [cacheUntil] : []
        };
    }
  }

  // Check if we need to bust the cache
  function _shouldBust(key) {
    // Bust the cache if any of the item's cache busters return true
    return cache[key] && cache[key].busters ? cache[key].busters.some(_returnsTrue) : false;
  }

  function _returnsTrue(fn) {
    return !!fn();
  }

  window.addEventListener('hashchange', function () {
    for (var url in cache) {
        if (_shouldBust(url)) {
            cache[url].subject.onCompleted();
            delete cache[url];
        }
    }
  });

  function fetcher$1() {
    var args = Array.prototype.slice.call(arguments);
    var url = args[0] instanceof Request ? args[0].url : args[0];

    // We do not support retrying a fetch call using a Request instance yet
    if (args[0] instanceof Request) {
        console.warn('fetcher calls using a Request instance are not supported. A regular fetch call has been returned.');
        return fetch.apply(null, args);
    }

    if (!args[1]) {
        args.push({});
    }

    // Sets the headers correctly on the call
    if (!args[1].headers) {
        args[1].headers = new Headers();
    } else if (!(args[1] instanceof Headers)) {
        args[1].headers = new Headers(args[1].headers);
    }

    args[1].headers.set('X-CSRF-TOKEN', auth.getCSRFToken());

    if (!args[1].credentials) {
        args[1].credentials = 'include';
    }

    return fetch.apply(null, args).then(function (response) {
        if (response.status === 401) {
            return auth.refreshAuthToken({ clientSecret: 'TaxUI:f7fsf29adsy9fg' }).then(fetcher$1.bind.apply(fetcher$1, [null].concat(_toConsumableArray(args))));
        }

        return Promise.resolve(response).then(function (resp) {

            // If this is a GET, PUT, or PATCH on a url that's in the cache, we need to update the cached Subject to reflect the new data.
            // The API's PUT/PATCH response must be the complete object.
            if (isCached(url) && (!args[1].method || args[1].method.toLowerCase() === 'put' || args[1].method.toLowerCase() === 'patch' || args[1].method.toLowerCase() === 'get')) {
                (function () {
                    var oldRespJson = resp.json;

                    resp.json = function () {
                        return new Promise(function (resolve, reject) {
                            oldRespJson.call(resp).then(function (json) {
                                updateCache(url, json);
                                resolve(json);
                            }).catch(reject);
                        });
                    };
                })();
            }

            // The PUT/PATCH may also affect cached items that share the same base URL (but with a different query string). Those should be invalidated/updated.
            var affectedItems = getAffectedCachedItems(url);
            if (!lodash.isEmpty(affectedItems) && args.length >= 2 && args[1].method && (args[1].method.toLowerCase() === 'put' || args[1].method.toLowerCase() === 'patch')) {
                for (var key in affectedItems) {
                    // Re-get them using the forceBust option to update the cache
                    getWithSharedCache(key, null, true);
                }
            }

            return resp;
        });
    });
  }

  window.fetcher = fetcher$1;

  exports['default'] = fetcher$1;
  exports.getWithSharedCache = getWithSharedCache;

});

@joeldenning did you test the fix added in systemjs/builder@550e22c? That should exactly ensure that the AMD output does exports = fetcher$1 instead of exports['default'] = fetcher$1.

You may hit a Rollup bug though as it looks like getWithSharedCache is a named export, which Rollup may not like.

Hmm actually sorry ignore my previous comment. exports['default'] = fetcher$1 is the correct output and should still be what you get here with the previous change.

Rather, the issue is that Rollup is not outputting the __esModule flag.

The workaround would be to add export const __esModule = true to the fetcher module.

The actual issue is tracking in rollup/rollup#650.

Thanks for pointing me to that issue in rollup. I'll stick with --skip-rollup or do the export const __esModule = true workaround in the meantime.

👍 ok going ahead with closing this then, as it's a separate Rollup issue.