Internal Javascript Libraries

In which situation do I need a library ?

  • When you need to share code between two apps or endpoints

  • When the shared code has at least one NPM dependency

  • When the shared code needs translated strings (gettext)

  • When you want a common endpoint to include Javascript and CSS styles

  • When you need to share code between a TypeScript app and a plain Javascript app

  • When you want to share a Vue component

When NOT to create a library ?

  • When the code uses dynamic import, for example to load polyfills or translations. In this case, use a standard vite configuration

  • When you need to output a file with a revision hash in its name, for example my-lib-name-0123456aea.js. In this case, use a standard vite configuration.

Folder structure of an internal library

Create a scripts/lib/ folder in Tuleap Core or in the plugin where code is shared:

# In core
$> mkdir -p tuleap/src/scripts/lib/my-lib-name/ && cd tuleap/src/scripts/lib/my-lib-name/
# In a plugin
$> mkdir -p tuleap/plugins/my-plugin/scripts/lib/my-lib-name/ && cd tuleap/plugins/my-plugin/scripts/lib/my-lib-name/

Here is the folder structure you should follow:

my-plugin/
 |-- build-manifest.json # Edit it to declare your lib for translations
 |-- scripts/
      |-- lib/
           |-- my-lib-name/
                |-- .gitignore          # Exclude dist/ from git
                |-- jest.config.js      # Unit tests bootstrapping
                |-- package.json        # Declares the library name, its dependencies and its build scripts.
                |-- pnpm-lock.yaml      # Generated by pnpm. Never edit manually.
                |-- tsconfig.json       # Typescript configuration
                |-- vite.config.ts      # Vite configuration to build the lib
                |-- images/                             # Images to include in the lib's CSS
                     |-- some-image.png
                |-- dist/                               # Generated assets. Must be excluded from git
                     |-- my-lib-name.umd.js             # Javascript UMD bundle, it is referenced in "main" in package.json
                     |-- my-lib-name.umd.d.js           # Typescript declarations entrypoint for my-lib-name.umd.js
                     |-- my-lib-name.es.js              # Javascript ES module bundle, it is referenced in "module" in package.json
                     |-- my-lib-name.es.d.js            # Typescript declarations entrypoint for my-lib-name.es.js
                     |-- style.css                      # CSS bundle, it is referenced in "style" in package.json
                |-- po/                                 # Localization strings
                     |-- fr_FR.po                       # Localized strings for French
                |-- src/                                # The lib source-code
                     |-- main.ts                        # Entrypoint for your library
                     |-- subfolder/
                          |-- my-other-source.ts
                |-- themes/                             # The lib styles
                     |-- style.scss                     # Entrypoint for your library styles

Build your internal library

The build system will read build-manifest.json to understand how and where it needs to extract translated strings.

// tuleap/plugins/my-plugin/build-manifest.json
{
    "name": "my-plugin",
    "gettext-ts": {
        "my-lib-name": {
            "src": "src/scripts/lib/my-lib-name/src",
            "po": "src/scripts/lib/my-lib-name/po"
        }
    }
}

Create manually the fr_FR.po file. When you run make generate-po, this file is NOT created but it will be filled with the translations.

// tuleap/plugins/my-plugin/po/fr_FR.po
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: Your Full Name <your email address>\n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

To build up your application, you will have to create a vite.config.ts file. This file should be located in my-lib-name/.

// tuleap/plugins/my-plugin/scripts/lib/my-lib-name/vite.config.ts
import { vite, viteDtsPlugin } from "@tuleap/build-system-configurator";
import * as path from "path";

export default vite.defineLibConfig({
    plugins: [viteDtsPlugin()],
    build: {
        lib: {
            entry: path.resolve(__dirname, "src/main.ts"),
            name: "MyLibName",
        },
        // Exclude an external dependency from the lib's bundle
        rollupOptions: {
            external: ["dompurify"],
            output: {
                globals: {
                    dompurify: "DOMPurify",
                },
            },
        },
    }
});

Once you have a Vite config, you will need a package.json in my-lib-name/.

// tuleap/plugins/my-plugin/scripts/lib/my-lib-name/package.json
{
  "author": "Enalean Team",                   // or yourself
  "name": "@tuleap/my-lib-name",
  "homepage": "https://tuleap.org",           // or your lib's homepage
  "license": "GPL-2.0-or-later",              // or your license
  "private": true,                            // to avoid accidentally publishing on NPM registry
  "type": "module",                           // Allow import/export instead of require()
  "module": "dist/my-lib-name.js",            // The Javascript ES Module bundle of your lib
  "main": "dist/my-lib-name.umd.cjs",         // The Javascript UMD bundle of your lib
  "types": "dist/main.d.ts",                  // Generated TypeScript declarations
  "exports": {
    ".": {
      "import": "./dist/my-lib-name.js",      // The Javascript ES Module bundle of your lib
      "require": "./dist/my-lib-name.umd.cjs" // The Javascript UMD bundle of your lib
    }
  },
  "style": "dist/style.css",                  // The CSS bundle of your lib
  "dependencies": {
    "dompurify": "^2.3.4"
  },
  "devDependencies": {
    "@tuleap/build-system-configurator": "workspace:*",
    "@types/dompurify": "^2.3.2"
  },
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "vite build",
    "watch": "vite build --watch --mode development --minify false",
    "test": "jest"
  }
}

Note

All the Vite and Jest dependencies are available at the tuleap root folder.

Use the pnpm scripts to build the library or to run the unit tests.

pnpm run typecheck # Run TypeScript type check on your code and unit tests.
pnpm run build     # For a production build, outputs minified code.
pnpm run watch     # Build the lib in watch mode.
pnpm test          # Run the Jest unit tests only once.

Warning

In order to test the library in real conditions (with your browser), you need to also include it in an application AND also rebuild that application.

Once you have a package.json file, you will also need a tsconfig.json file to configure Typescript.

// tuleap/plugins/my-plugin/scripts/lib/my-lib-name/tsconfig.json
{
    "extends": "../../../../../tools/utils/scripts/tsconfig-for-libraries.json",
    "compilerOptions": {
        "lib": [],          // Add values like "DOM" if your lib interacts with the DOM
        "types": ["jest"],  // Add global types needed by your lib
    },
    "include": ["src/**/*"]
}

You also need a Jest config, but this one has nothing special.

// tuleap/plugins/my-plugin/scripts/lib/my-lib-name/jest.config.js
import { env } from "node:process";
import { defineJestConfiguration } from "@tuleap/build-system-configurator";

env.DISABLE_TS_TYPECHECK = "true";

const jest_base_config = defineJestConfiguration();
module.exports = {
    ...jest_base_config,
    displayName: "my-lib-name",
};

Add a .gitignore file to remove the dist/ folder from source control. It contains only generated files and should not be committed.

// tuleap/plugins/my-plugin/scripts/lib/my-lib-name/.gitignore
dist/

If you have gettext translations with node-gettext, you will need a pofile-shim.d.ts so that TypeScript understands what is returned by import "file.po".

// tuleap/plugins/my-plugin/scripts/lib/my-lib-name/src/pofile-shim.d.ts
declare module "*.po" {
    import type { GettextParserPoFile } from "@tuleap/gettext";
    const content: GettextParserPoFile;
    export default content;
}

In your stylesheet, you can reference images. They will be inlined (converted to a base64 string) and included in dist/style.css.

// tuleap/plugins/my-plugin/scripts/lib/themes/style.scss
.some-css-class {
    // The image will be converted to a base64 string
    background: url('../images/some-image.png');
}

Finally, your main.ts file (the lib entrypoint) should export types that callers will need. Exporting them will ensure that the generated main.d.ts declaration file references those types. Also note that you need to import the style file you referenced in your package.json so it can be processed by Vite.

// tuleap/plugins/my-plugin/scripts/lib/my-lib-name/src/index.ts
import "../themes/style.scss"; // Import the styles to bundle them in dist/style.css
import type { MyType, MyOtherType } from "./types";

export type { MyType, MyOtherType }; // Re-export the types, so that TypeScript callers can import them
export function myFunction(param: MyType): MyOtherType {
    //...
}

Use your library from another application

To use your library from another application, you must first declare it as a dependency in the app’s package.json file. Use pnpm add @tuleap/my-lib-name@* to achieve that. You will get something looking like this:

// tuleap/plugins/other-plugin/package.json
{
  "name": "@tuleap/other-plugin",
  // ...
  "dependencies": {
    "@tuleap/my-lib-name": "workspace:*" // Add your lib as a dependency
  },
  "scripts": {
    "build": "..."
  }
}

Use the library like any other “npm module” in Javascript / Typescript files:

// tuleap/plugins/other-plugin/scripts/other-app/src/other-file.ts
import type { MyOtherType } from "@tuleap/my-lib-name";
import { myFunction } from "@tuleap/my-lib-name";

const result: MyOtherType = myFunction(param);

Import the CSS styles like any other “npm module” in SCSS files:

// tuleap/plugins/other-plugin/themes/BurningParrot/src/other-file.scss
@use '@tuleap/my-lib-name';

Making changes to your library

Warning

While working on your library, changes will NOT be automatically visible from the application. Both the library and the application MUST be rebuilt in order to see your changes.

$> (cd tuleap/plugins/my-plugin/scripts/lib/my-lib-name/ && pnpm run watch) & (cd tuleap/plugins/other-plugin/ && pnpm run watch) && fg
# CTRL-C twice to exit both watches