Search code examples
d3.jsrollupes6-modules

ES6 modules and decorator pattern


I understand that the import statement delivers a read-only binding to a module and I guess it depends on the module loader, but is it intended to be possible to import, decorate and re-export using ES6 modules?

For example, this fails when using rollup.js


test.plugin.js

import * as d3 from 'd3'    
d3.ui = {test: 1};    
export default d3;

index.js

import * as d3 from 'd3'
import './src/test.plugin'

Rollup error...

Illegal reassignment to import 'd3'
src\test.plugin.js (6:0)
4:
5: import * as d3 from 'd3'
6: d3.ui = {test: 1};

And so does this...

test.plugin.js
export default (d3) => d3.ui = {test: 1};

index.js

import * as d3 from 'd3'
import test from './src/test.plugin'

test(d3);

The first one fails because the imports are immutable and the second because the module resolution is static.

Is it supposed to be possible to use the decorator pattern with ES6 modules?


Solution

  • Update

    The problem is that module objects are not extensible. What is extensible is instead an object inside the module object.

    Module A

    let mod = { a, b, c };
    // Once exported the "mod" object cannot be extended from the outside
    export mod;
    

    index.js

    // What you are saying here is 
    import * as mod from "moduleA.js"
    
    // mod cannot be extended here
    // mod.b can be extended though
    mod.b.ui = {test: 1};
    

    Default export

    When you do the default export you can extend it as default is effectively a nested property.

    Module A

    let mod = { a, b, c };
    // Once exported as default
    export default mod;
    

    index.js

     import mod from "moduleA.js"
    
     // mod is effectively a prop of the module object, so it can be extended
     mod.d = { ... };
    

    In your case you can do the following:

    test.plugin.js

    // Import d3 as a composition of props
    import * as d3 from 'd3';
    // Create a new object using the Object.assign operator
    // You can use the spread operator too
    const d3plus = Object.assign({ui: () => 'test'}, d3);
    // Now d3plus will be extendable!
    export default d3plus;
    

    index.js

    import d3plus from 'test.plugin.js';
    
    console.log(d3plus.ui);
    

    Old wrong answer

    This is my answer when I got it wrong reading the spec. To be fair also some other module bundler got it wrong before hand, as es6 modules are pretty hard.

    When you have module A and you want to decorate it with new functions/things you have two choices:

    • Wrap the module A and then use the wrapper only later on
    • Write decorator functions you apply when you need it (as in your second example).

    In the former case you can do:

    test.plugin.js

    import * as d3 from 'd3'    
    d3.ui = {test: 1};    
    export default d3;
    

    index.js

    // Note that using this wrapper makes sure you have the extra stuff all the time
    import d3plus from './src/test.plugin';
    
    console.log(d3plus.ui);
    

    With the second approach you have to get the result of the decorator operation:

    test.plugin.js

    export default (d3) => {
      d3.ui = {test: 1};
      // Do not forget to return the new object
      return d3;
    };
    

    index.js

    import * as d3 from 'd3'
    import pluginify from './src/test.plugin'
    
    // Note that this change is local to this module only
    const d3plus = pluginify(d3);
    console.log(d3plus.ui);
    

    You may use some more tricks to achieve the same result but I would recommend to make it explicit the enrichment process you are applying to the module.