scorpp
scorpp

Reputation: 668

How to migrate legacy JS app to modules

I have a large (~15k LoC) JS app (namely a NetSuite app) written in old-style all-global way. App consists of 26 files and dependencies between them are totally unclear.

The goal is to gracefully refactor the app to smaller modules. By gracefully I mean not breaking\locking the app for long time, but doing refactoring in smaller chunks, while after completing each chunk app remains usable.

An idea I have here is to concat all the JS files we have now into single-file bundle. After that some code could be extracted into modules. And the legacy code could start importing it. The modules & imports should be transpiled with webpack\whatever, while legacy code remains all-globals style. Finally all this is packed into single JS file and deployed.

My questions are

  1. is there a better approach maybe? This sounds like a typical problem
  2. are there any tools available to support my approach?

I gave webpack a try and I haven't managed to get what I want out of it. The export-loader and resolve-loader are no options because of amount of methods\vars that needs to be imported\exported.

Examples

Now code looks like

function someGlobalFunction() {
  ...
}

var myVar = 'something';
// and other 15k lines in 26 files like this

What I would ideally like to achieve is

function define(...) { /* function to define a module */ }

function require(moduleName) { /* function to import a module */ }

// block with my refactored out module definitions

define('module1', function () {
  // extracted modularised code goes here
});

define('module2', function () {
  // extracted modularised code goes here
});

// further down goes legacy code, which can import new modules

var myModule = require('myNewModule');
function myGlobalLegacyFunction() {
   // use myModule
}

Upvotes: 8

Views: 1623

Answers (2)

Gunther Schadow
Gunther Schadow

Reputation: 1739

No good answer here so far, and it would be great if the person asking the question would come back. I will pose a challenging answer saying that it cannot be done.

All module techniques end up breaking the sequential nature of execution of scripts in the document header.

All dynamically added scripts are loaded in parallel and they do not wait for one another. Since you can be certain that almost all such horrible legacy javascript code is dependent on the sequential execution, where the second script can depend on the first previous one, as soon as you load those dynamically, it can break.

If you use some module approach (either ES7 2018 modules or require.js or you own) you need to execute the code that depends on the loading having occurred in a call-back or Promise/then function block. This destroys the implicit global context, so all these spaghetti coils of global functions and var's we find in legacy javascript code files will not be defined in the global scope any more.

I have determined that only two tricks could allow a smooth transition:

Either some way to pause continuation of a script block until the import Promise is resolved.

const promise = require("dep1.js", "dep2.js", "dep3.js");
await promise;
// legacy stuff follows

or some way to revert the scope of a block inside a function explicitly into the global scope.

with(window) {
    function foo() { return 123; }
    var bar = 543;
}

But neither wish was granted by the javascript fairy.

In fact, I read that even the await keyword essentially just packs the rest of the statements into function to call when promise is resolved:

async function() {
    ... aaa makes promise ...
    await promise; 
    ... bbb ...
}

is just, I suppose, no different from

async function() {
    ... aaa makes promise ...
    promise.then(r => {
        ... bbb ...
    });
}

So this means, the only way to fix this is by putting legacy javascript statically in the head/script elements, and slowly moving things into modules, but continue to load them statically.

I am tinkering with my own module style:

(function(scope = {}) {
    var v1 = ...;
    function fn1() { ... }
    var v2 = ...;
    function fn2() { ... }

    return ['v1', 'fn1', 'v2', 'fn2']
           .reduce((r, n) => { 
                        r[n] = eval(n); 
                        return r; 
                    }, scope);
})(window)

by calling this "module" function with the window object, the exported items would be put on there just as legacy code would do.

I gleaned a lot of this by using knockout.js and working with the source readable file that has everything together but in such module function calls, ultimately all features are on the "ko" object.

I hate using frameworks and "compilation" so generating the sequence of HTML tags to load them in the correct order by the topologically sorted dependency tree, while I could write myself such a thing quickly, I won't do this, because I do not want to have any "compilation" step, not even my own.

UPDATE: https://stackoverflow.com/a/33670019/7666635 gives the idea that we can just Object.assign(window, module) which is somewhat similar to my trick passing the window object into the "module" function.

Upvotes: 0

rwold
rwold

Reputation: 2476

I'm following an approach similar to that outlined here: https://zirho.github.io/2016/08/13/webpack-to-legacy/

In brief:

Assuming that you can configure webpack to turn something like

export function myFunction(){...}

into a file bundle.js that a browser understands. In webpack's entry point, you can import everything from your module, and assign it to the window object:

// using namespace import to get all exported things from the file
import * as Utils from './utils'

// injecting every function exported from utils.js into global scope(window)
Object.assign(window, Utils).

Then, in your html, make sure to include the webpack output before the existing code:

<script type="text/javascript" src="bundle.js"></script>
<script type="text/javascript" src="legacy.js"></script>

Your IDE should be able to help identify clients of a method as you bring them into a module. As you move a function from legacy.js to myNiceModule.js, check to see if it still has clients that are aware of it globally - if it doesn't, then it doesn't need to be globally available.

Upvotes: 3

Related Questions