Как использовать Google Closure Compiler для просмотра вашей библиотеки Node.js

У меня есть эта простая библиотека Node.js:

mylib/
|- inc.js
|- index.js
|- is_number.js
|- package.json

mylib/is_number.js

module.exports = x => typeof x === 'number';

mylib/inc.js

const is_number = require('./is_number');

module.exports = x => is_number(x) ? x + 1 : x;

mylib/index.js (значение свойства main в моем package.json)

module.exports = {
  inc: require('./inc'),
  utils: {
    is_number: require('./is_number')
  }
};

Пример:

const mylib = require('mylib');

mylib.inc(41);
//=> 42

mylib.utils.is_number(42);
//=> true

Как я могу использовать Google Closure Compiler для просмотра моей библиотеки Node.js, чтобы она могла работать и в браузере? например.,

<script src="mylib/browser.min.js"></script>
<script>
const mylib = window.mylib;

mylib.inc(41);
//=> 42

mylib.utils.is_number(42);
//=> true
</script>

person customcommander    schedule 02.02.2021    source источник


Ответы (1)


Каноническим сообщением для этого ответа является Gist.

TL; DR

  1. Создать mylib/index_browser.js

    window.mylib = {
      inc: require('./inc'),
      utils: {
        is_number: require('./is_number')
      }
    };
    
  2. Создать mylib/externs.js

    /** @externs */
    var mylib;
    var inc;
    var utils;
    var is_number;
    
  3. Затем:

    $ cc --compilation_level ADVANCED \
        --language_out ES5 \
        --process_common_js_modules \
        --module_resolution NODE \
        --externs mylib/externs.js \
        --isolation_mode IIFE \
        --js mylib/index_browser.js mylib/inc.js mylib/is_number.js \
        --js_output_file mylib/browser.min.js
    

    Где cc — это псевдоним вашего экземпляра Google Closure Compiler; см. пример ниже


Прежде чем мы начнем:

Я написал этот псевдоним, чтобы упростить вызов Google Closure Compiler (CC).

$ alias cc="java -jar /devtools/closure-compiler/compiler.jar"
$ cc --version
Closure Compiler (http://github.com/google/closure-compiler)
Version: v20210106

Браузерная версия библиотеки будет скомпилирована до ES5.

Пошаговые инструкции

Ваша первая попытка может выглядеть так: просто скомпилируйте файл экспорта mylib/index.js

$ cc --compilation_level ADVANCED \
     --language_out ES5 \
     --js mylib/index.js
mylib/index.js:1:0: ERROR - [JSC_UNDEFINED_VARIABLE] variable module is undeclared
  1| module.exports = {
     ^^^^^^

mylib/index.js:2:7: ERROR - [JSC_UNDEFINED_VARIABLE] variable require is undeclared
  2|   inc: require('./inc'),
            ^^^^^^^

2 error(s), 0 warning(s)

Если КК не знает о module и require, это не самое лучшее начало.

К счастью, нам не хватает только флага --process_common_js_modules:

$ cc --compilation_level ADVANCED \
     --language_out ES5 \
     --process_common_js_modules \
     --js mylib/index.js
mylib/index.js:2:7: ERROR - [JSC_JS_MODULE_LOAD_WARNING] Failed to load module "./inc"
  2|   inc: require('./inc'),
            ^

mylib/index.js:4:15: ERROR - [JSC_JS_MODULE_LOAD_WARNING] Failed to load module "./is_number"
  4|     is_number: require('./is_number')
                    ^

2 error(s), 0 warning(s)

Все еще не очень хорошо, но на этот раз ошибки другие:

  1. Кики не знает, о каком require ты говоришь
  2. CC не знает, где находятся эти два других модуля.

Нам нужен флаг --module_resolution и указать CC, где находятся остальные модули:

$ cc --compilation_level ADVANCED \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --js mylib/index.js mylib/inc.js mylib/is_number.js 

However the output is empty...

Why? In ADVANCED compilation mode CC removes any code that is not used. Which is the case actually: so far all this stuff isn't used at all!

Давайте проверим с менее агрессивным режимом компиляции:

$ cc --compilation_level WHITESPACE_ONLY --formatting PRETTY_PRINT \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --js mylib/index.js mylib/inc.js mylib/is_number.js 
var module$mylib$index = {default:{}};
module$mylib$index.default.inc = module$mylib$inc.default;
module$mylib$index.default.utils = {is_number:module$mylib$is_number.default};
var module$mylib$inc = {};
var is_number$$module$mylib$inc = module$mylib$is_number.default;
module$mylib$inc.default = function(x) {
  return (0,module$mylib$is_number.default)(x) ? x + 1 : x;
};
var module$mylib$is_number = {};
module$mylib$is_number.default = function(x) {
  return typeof x === "number";
};

Мы видим, что даже если бы режим компиляции ADVANCED не удалял все, это все равно не было бы очень полезным. Где window.mylib например?

Единственный способ, которым мне удалось получить доступ к моей библиотеке по адресу window.mylib и скомпилировать с самым агрессивным режимом компиляции, — это иметь отдельный файл экспорта для браузера.

Из этого mylib/index.js

module.exports = {
  inc: require('./inc'),
  utils: {
    is_number: require('./is_number')
  }
};

К этому mylib/index_browser.js

window.mylib = {
  inc: require('./inc'),
  utils: {
    is_number: require('./is_number')
  }
};

Когда вы добавляете к объекту window, CC знает, что этот код может быть достигнут, поэтому он больше не может безопасно удалить его.

Попробуем еще раз с этим файлом:

$ cc --compilation_level ADVANCED --formatting PRETTY_PRINT \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --js mylib/index_browser.js mylib/inc.js mylib/is_number.js
function b(a) {
  return "number" === typeof a;
}
;window.g = {h:function(a) {
  return b(a) ? a + 1 : a;
}, j:{i:b}};

Выглядит лучше, но есть большая проблема: CC исказил все имена!

Не волнуйся! Нам нужно только сказать, какие имена CC следует оставить в покое. Это цель файла externs.

mylib/externs.js

/** @externs */
var foo;
var inc;
var utils;
var is_number;

Нам нужен еще один флаг: --externs

$ cc --compilation_level ADVANCED --formatting PRETTY_PRINT \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --externs mylib/externs.js \
     --js mylib/index_browser.js mylib/inc.js mylib/is_number.js
function b(a) {
  return "number" === typeof a;
}
;window.mylib = {inc:function(a) {
  return b(a) ? a + 1 : a;
}, utils:{is_number:b}};

Попасть туда...

Одно из очевидных улучшений заключается в том, чтобы обернуть все это в IIFE, чтобы избежать чрезмерного загрязнения глобальной области видимости.

Нам нужен флаг --isolation_mode:

$ cc --compilation_level ADVANCED --formatting PRETTY_PRINT \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --externs mylib/externs.js \
     --isolation_mode IIFE \
     --js mylib/index_browser.js mylib/inc.js mylib/is_number.js
(function(){function b(a) {
  return "number" === typeof a;
}
;window.mylib = {inc:function(a) {
  return b(a) ? a + 1 : a;
}, utils:{is_number:b}};
}).call(this);

Фантастика!

Все, что осталось сделать, это сохранить это в файл и удалить форматирование, чтобы сэкономить несколько лишних байтов:

$ cc --compilation_level ADVANCED \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --externs mylib/externs.js \
     --isolation_mode IIFE \
     --js mylib/index_browser.js mylib/inc.js mylib/is_number.js \
     --js_output_file mylib/browser.min.js

mylib/browser.min.js

(function(){function b(a){return"number"===typeof a};window.mylib={inc:function(a){return b(a)?a+1:a},utils:{is_number:b}};}).call(this);
person customcommander    schedule 02.02.2021