![LICENSE](https://img.shields.io/badge/Lisence-MIT-blue.svg)
[![Node.js CI](https://github.com/jeffy-g/compressed-script-loader/actions/workflows/node.js.yml/badge.svg)](https://github.com/jeffy-g/compressed-script-loader/actions/workflows/node.js.yml)
[![npm version](https://badge.fury.io/js/compressed-script-loader.svg)](https://badge.fury.io/js/compressed-script-loader)
![npm bundle size](https://img.shields.io/bundlephobia/min/compressed-script-loader?style=plastic)
![npm](https://img.shields.io/npm/dm/compressed-script-loader.svg?style=plastic)

[1]:https://github.com/101arrowz/fflate
[2]:https://www.w3.org/TR/SRI/

# ![jet](https://user-images.githubusercontent.com/26692481/169954005-9db1b434-02ca-46a8-a540-a2add88a3905.png) compressed-script-loader

Reduces network traffic for huge source files built by webpack etc.

## ✨ Motivation

Modern web servers often support compression via `content-encoding`.  
However, on servers that do not handle compression transfer, this module can significantly reduce network traffic.

## 🔥 Installation

```bash
npm install compressed-script-loader
```

## 📇 Details

  + Use code with **comment** extraction feature added to `unzipSync` of [`fflate`][1].

  + **comment** is used to embed [**SRI**][2]. (`script.integrity`)  
If comment is omitted or does not pass to verify process that it is [**SRI**][2],  
the unzipped code will be bound to the `text` property of `script` element.  
You can use `sha256`, `sha384`, and `sha512` in `script.integrity`, and the prefix `sha###-` is omit able because the hash method is automatically detected by the length of the [**SRI**][2].
    + [SRI online tool](https://www.srihash.org/)

  + example `script` element by `compressed-script-loader`

    ```html
    <script id="system-coordinate-map-mini.js"
      integrity="sha384-X21iVQnEaPD0mdNEF66lHUBS4f63TQQHaDVxvRwIDm4yi+IE+ulVIpDJDAoKW3BP"
      crossorigin="anonymous"
      src="blob:https://jeffy-g.github.io/b4e7bdbd-f4c8-41af-b170-93fb979317fd">
    </script>
    ```

  + [**`cslpack`**](#cslpack-cli-since-v12x) cli - simple zip tool is available (since v1.2.x)  

  + support module pack (experimental, since v1.3.x)

    + `importmap` is auto generate


## 🖌️ TODO

  + `type="module"`
    - stage 1 (v1.3.4, unstable)
    - stage 2 (v1.4.8, unstable)
      * Module id mapped by "baseUrl" and "paths" in tsconfig.json can now be resolved.
      ```jsonc
       // 
       {
         "compilerOptions": {
           // 
           "baseUrl": ".",
           "paths": {
             "eveworld/*": [
               "./*"
             ],
             "web/*": [
               "./web/*"
             ]
           }
         }
       }
      ```

      note:
        1. Filename only recognizes "tsconfig.json".
        2. At a minimum, `compilerOptions.baseUrl` and `compilerOptions.paths` are required.
        3. No matter where "tsconfig.json" is located, if `compilerOptions.baseUrl` is set correctly, there will be no error! (Probably...)

    - stage 3 (v1.4.28)
      * **Major Leap in Module ID Detection** (commit `ce3c25f`):
        The module ID detection logic has been overhauled with a comprehensive, typed regex (powered by `js-dev-tool/regex`).
        It now robustly handles:
        - **Comments**: Correctly ignores imports within single-line (`//`) and multi-line (`/* ... */`) comments.
        - **Dynamic Imports**: Supports `import("...")` and `import('...')` syntax.
        - **Complex Patterns**: Utilizes named capture groups for precise extraction of global, dynamic, and ESM module IDs.
        This update resolves previous limitations and ensures accurate dependency mapping for `importmap` generation.

## 🦯 Use for SPA

  > #### Load automatically

  + Specifying config in the `data-csl-config` attribute of the `script` element  
    will automatically load the zip file and insert it as a `script` element in the header section of the html page.

  ```html
  <script>
    let onLoadDone = () => {
      onLoadDone = void 0;
      runEVEWorld(() => {
        window.setTimeout(() => {
          document.querySelectorAll("script[src^='./loader/'],script[src*='compressed-script-loader']").forEach(script => script.remove());
          delete window.NsLoader;
        }, 1000);
      });
    };
  </script>
  <script
    data-csl-config='{
      "base": "./libs",
      "selector": "script[src*=css-2d-renderer]",
      "load": [
        "system-coordinate-map-mini"
      ],
      "callback": "onLoadDone"
    }'
    type="text/javascript"
    src="https://cdn.jsdelivr.net/npm/compressed-script-loader/umd/index.js"
  ></script>
  ```

  + vscode snippet

```json
"compressed script loader snippet": {
  "prefix": "csl-script-tag",
  "scope": "markdown,html,json,jsonc,snippets",
  "description": "Insert compressed script loader script tag.",
  "body": [
    "<script data-csl-config='{",
    "  \"base\": \"${1:./lib}\",",
    "  \"load\": ${2|\"bundle\",[\"bundle\"]|},",
    "  ${3|\"callback\": \"cslCallback\",\"callback\": \"cslCallback\"&#44;\n\"cleanup\": true|}",
    "}' src=\"https://cdn.jsdelivr.net/npm/compressed-script-loader/umd/index.js\" defer></script>"
  ]
}
```


  + Supports simple `data-csl-config` (since v1.2.6)
    + In this case, the packed zip file must be deployed in the same directory as the page and  
      If need a callback, define it as `cslCallback`.

  ```html
  <script
    data-csl-config='"system-coordinate-map-mini"'
    src="https://cdn.jsdelivr.net/npm/compressed-script-loader@1.2.6/umd/index.js"
  ></script>
  <!-- or -->
  <script
    data-csl-config='["system-coordinate-map-mini", "bundle"]'
    src="https://cdn.jsdelivr.net/npm/compressed-script-loader@1.2.6/umd/index.js"
  ></script>
  ```

  > #### Load manually

  ```html
  <script src="https://cdn.jsdelivr.net/npm/compressed-script-loader/umd/index.js"></script>
  <script>
  // global variable `NsLoader` is available (umd)
  (async () => {
      const ZIPNAME = "system-coordinate-map-mini";
      // If the packed zip file is located in the same directory as the page
      // and you don't care where the script is inserted, you can skip this step.
      NsLoader.setConfig("./libs"/*, "script[src*=css-2d-renderer]"*/);

      await NsLoader.loadCompressedScript(ZIPNAME);
      // If you don't use it anymore, this step will destroy the loader api
      NsLoader.cleanUp();

      runEVEWorld(() => {
          window.setTimeout(() => {
            document.querySelectorAll("script[src^='./loader/'],script[src*='compressed-script-loader']").forEach(script => script.remove());
            delete window.NsLoader;
          }, 1000);
      });
  })();
  </script>
  ```

## ![command-line](https://user-images.githubusercontent.com/26692481/169953084-fe079cf1-362b-4c1c-be5c-4dc5fe305f98.png) `cslpack` cli (since v1.2.x)

uses [fflate][1]'s `zipSync` feature

`$ cslpack <jspath> <jspath>... [-(d|dest) <output zip path>] [-sha <256|384|512>] [-m <main module name with extension>]`

+ options
  * `-(d|dest)`- output zip path. when omited will be output to **`./dist/bundle.zip`**
  + `-sha`     - [256|384|512] when omited use 384 "sha384" (since v1.2.8)
  + `-m`       - When packing as an esm module, specify the script name such as `main.js` that will be the entry point.      pack as esm module with "importmap" data.  
    "importmap" is auto generate and pack together. (since v1.3 experimental)

**example:**

```shell
# apply just `sha384`
$ cslpack -d dist/lib/bundle.zip build/{bundle,data}.js

# or
$ cslpack build/{cjs,esm}/* -dest dist/lib/bundle.zip -sha 512

# will be output to "./dist/bundle.zip"
$ cslpack build/{cjs,esm}/*

# pack as `esm` module. `main.js` is `build/esm/main.js`
$ clspack build/esm/*.js build/lib/hoge-data.js -m main.js 
```

> NOTE: \`cslpack\` does not keep directory path


## ![gear](https://user-images.githubusercontent.com/26692481/169953504-f24a7d4d-6f37-41f5-b889-e238f1cc1efe.png) Loader API

  ```ts
  declare global {
      var cslCallback: TCSLCallbak;
      interface Window {
          cslCallback: TCSLCallbak;
          [cslCallbackName: string]: TCSLCallbak;
      }
  }
  export declare type TCSLCallbak = ((err?: Error[]) => void) | undefined;
  export interface UnzipFileInfo {
      name: string;
      size: number;
      originalSize: number;
      compression: number;
      /**
       * @since v1.2.9
       */
      comment?: string;
  }
  export declare type UnzipFileFilter = (file: UnzipFileInfo) => boolean;
  export interface UnzipOptions {
      filter?: UnzipFileFilter;
  }
  export interface Unzipped {
      [path: string]: {
          data: Uint8Array;
          comment?: string;
      };
  }
  export declare type TCSLConfig = {
      base: string;
      selector?: string;
      load: string[];
      callback?: string;
      cleanup?: true;
  };

  declare var NsLoader: {
      unzipSync: (data: Uint8Array, opts?: UnzipOptions) => Unzipped;
      cleanUp: () => void;
      setConfig: (base: string, insertionSelector?: string | undefined) => void;
      loadCompressedScript: (baseName: string, log?: (msg: string) => void) => Promise<void>;
      readonly version: string;
  };
  declare interface Window {
      NsLoader: typeof NsLoader;
  }
  ```

## ![settings](https://user-images.githubusercontent.com/26692481/169953770-76da1cb2-9159-4d68-94dc-b4a4be77bbce.png) Config definition

  ```ts
  /**
  * (C)ompressed-(S)cript-(L)oader config
  * 
  * @version 1.0
  */
  export type TCSLConfig = {
      /**
      * Specifies the directory that contains the zip file
      * 
      * ```js
      * base: "./lib" // or https://example.com/lib
      * ```
      */
      base: string;
      /**
      * #### Insertion selector
      * 
      *   + can be omit. In the current specification, specify element inside `head`  
      *     element to insert into` head` element.
      * 
      *   + If omitted, it will be inserted after `document.head.lastElementChild`
      * 
      *  NOTE: This will change in the near future
      * 
      * ```js
      * selector: "script[src*=css-2d-renderer]"
      * ```
      */
      selector?: string;
      /**
      * list of zip files you want to load
      * 
      *   + The comment attached to the zip file entry is used for `script.integrity`
      * 
      * NOTE: Omit the `.zip` extension
      * 
      * ```js
      * // actually, "system-coordinate-map-mini.zip", "bundle.zip"
      * load: ["system-coordinate-map-mini", "bundle"]
      * ```
      */
      load: string[];
      /**
       * can be omit, default: "cslCallback"
       * 
       * Please specify if you need to use another name
       * 
       * ```js
       * var cslCallback = () => {
       *    runEVEWorld(() => {
       *        window.setTimeout(() => {
       *            document.querySelectorAll("script[src^='./loader/'],script[src*='compressed-script-loader']").forEach(script => script.remove());
       *            delete window.NsLoader;
       *        }, 1000);
       *    });
       * };
       * ```
       */
      callback?: string;

      /**
       * need cleanup?
       * @default undefined
       */
      cleanup?: true;
  };
  ```

> ## Authors

-   **jeffy-g** - [jeffy-g](https://github.com/jeffy-g)

> ## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details

