Бандлинг CLI-утилит: shebang и banner

CLI-приложения (Command Line Interface) отличаются от обычных браузерных приложений тем, что запускаются непосредственно из терминала и часто распространяются через пакетный менеджер npm. При сборке таких программ возникает дополнительная задача: необходимо сохранить возможность запуска файла как исполняемого скрипта операционной системы.

Типичный исполняемый файл CLI-инструмента содержит специальную строку в самом начале:

#!/usr/bin/env node

Эта строка называется shebang. Она сообщает Unix-подобным системам, какой интерпретатор должен использоваться для выполнения файла.

При использовании Esbuild необходимо учитывать особенности обработки shebang и понимать механизм добавления служебного содержимого через параметр banner.


Что такое shebang

Shebang представляет собой первую строку файла, начинающуюся с символов #!.

Пример:

#!/usr/bin/env node

console.log('CLI запущен');

При запуске файла:

./cli.js

операционная система считывает первую строку и выполняет:

node cli.js

Использование конструкции env считается наиболее переносимым вариантом:

#!/usr/bin/env node

Вместо жёсткой привязки к пути:

#!/usr/bin/node

система самостоятельно найдёт установленный интерпретатор Node.js.


Структура CLI-проекта

Типичная структура проекта выглядит следующим образом:

project/
├── src/
│   └── cli.js
├── package.json
└── esbuild.config.js

Файл CLI:

#!/usr/bin/env node

console.log('Hello CLI');

В package.json обычно присутствует секция:

{
  "name": "my-cli",
  "bin": {
    "my-cli": "./dist/cli.js"
  }
}

После установки пакета команда становится доступной глобально:

my-cli

Сохранение shebang при сборке

Одним из преимуществ Esbuild является автоматическая обработка shebang.

Исходный файл:

#!/usr/bin/env node

import fs from 'fs';

console.log(fs.existsSync('.'));

Конфигурация:

import { build } from 'esbuild';

await build({
  entryPoints: ['src/cli.js'],
  bundle: true,
  outfile: 'dist/cli.js',
  platform: 'node'
});

После сборки строка shebang сохраняется:

#!/usr/bin/env node

"use strict";

// код бандла

Это позволяет продолжать использовать результат как полноценный исполняемый файл.


Проверка результата

После сборки можно проверить начало файла:

head -n 5 dist/cli.js

Ожидаемый результат:

#!/usr/bin/env node
"use strict";

Если shebang отсутствует, запуск через терминал напрямую может перестать работать.


Атрибуты исполняемого файла

На Unix-подобных системах одного shebang недостаточно.

Файлу необходимо выдать права на выполнение:

chmod +x dist/cli.js

После этого запуск становится возможен:

./dist/cli.js

Без флага исполнения появится ошибка:

Permission denied

Использование banner

Параметр banner позволяет добавлять произвольный текст в начало генерируемого файла.

Простейший пример:

await build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/index.js',
  banner: {
    js: '// Generated by Esbuild'
  }
});

Результат:

// Generated by Esbuild

"use strict";

// код приложения

Banner вставляется перед основным содержимым бандла.


Синтаксис banner

Параметр представляет собой объект с настройками для различных типов файлов.

Для Jav * aScript:

banner: {
  js: '// Build information'
}

Для CSS:

banner: {
  css: '/* Generated CSS */'
}

Можно одновременно использовать оба варианта:

banner: {
  js: '// JavaScript bundle',
  css: '/* CSS bundle */'
}

Добавление shebang через banner

Иногда исходный файл не содержит shebang, но итоговая сборка должна быть исполняемой.

В такой ситуации shebang можно вставить вручную:

await build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/cli.js',
  banner: {
    js: '#!/usr/bin/env node'
  }
});

Результат:

#!/usr/bin/env node
"use strict";

// код приложения

Подобный подход часто используется при генерации CLI-файлов из обычных модулей.


Совмещение shebang и комментариев

Banner может содержать несколько строк.

Пример:

banner: {
  js: `#!/usr/bin/env node
/**
 * My CLI Tool
 * Version 1.0.0
 */`
}

Результат:

#!/usr/bin/env node
/**
 * My CLI Tool
 * Version 1.0.0
 */

"use strict";

Это удобно для добавления служебной информации.


Встраивание информации о версии

Нередко CLI-программы содержат сведения о версии непосредственно в итоговом файле.

Пример:

import pkg from './package.json' assert { type: 'json' };

await build({
  entryPoints: ['src/cli.js'],
  bundle: true,
  outfile: 'dist/cli.js',
  banner: {
    js: `#!/usr/bin/env node
// Version: ${pkg.version}`
  }
});

Результат:

#!/usr/bin/env node
// Version: 2.4.1

Такой подход упрощает диагностику и аудит сборок.


Добавление лицензионной информации

Многие проекты обязаны распространять лицензионные уведомления.

Banner позволяет встроить их непосредственно в файл:

banner: {
  js: `/*!
 * My CLI Tool
 * Copyright (c) 2026
 * Licensed under MIT
 */`
}

Полученный файл начинается с лицензионного блока:

/*!
 * My CLI Tool
 * Copyright (c) 2026
 * Licensed under MIT
 */

Особенно полезно это при публикации библиотек и открытого программного обеспечения.


Esbuild поддерживает не только начало файла, но и его конец.

Пример:

await build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/index.js',
  banner: {
    js: '// BEGIN'
  },
  footer: {
    js: '// END'
  }
});

Результат:

// BEGIN

"use strict";

// код

// END

Для shebang подходит исключительно banner, поскольку shebang обязан находиться в первой строке файла.


Особенности при использовании формата ESM

CLI-приложения могут собираться в формате ECMAScript Modules.

Конфигурация:

await build({
  entryPoints: ['src/cli.js'],
  bundle: true,
  outfile: 'dist/cli.js',
  format: 'esm',
  platform: 'node'
});

При наличии shebang в исходном файле Esbuild сохранит его:

#!/usr/bin/env node

import fs from "fs";

Node.js корректно обрабатывает shebang и в ESM-файлах.


Особенности при использовании CommonJS

Для CommonJS ситуация аналогична.

Конфигурация:

await build({
  entryPoints: ['src/cli.js'],
  bundle: true,
  outfile: 'dist/cli.cjs',
  format: 'cjs',
  platform: 'node'
});

Результат:

#!/usr/bin/env node

"use strict";

Формат модуля не влияет на работу shebang.


Несколько входных точек CLI

Проект может содержать несколько исполняемых команд.

Структура:

src/
├── build.js
├── deploy.js
└── lint.js

Конфигурация:

await build({
  entryPoints: [
    'src/build.js',
    'src/deploy.js',
    'src/lint.js'
  ],
  outdir: 'dist',
  bundle: true,
  platform: 'node'
});

Если каждый файл содержит собственный shebang:

#!/usr/bin/env node

Esbuild сохранит его в каждом результирующем бандле.


Генерация универсального CLI-файла

Иногда исходный код представляет собой обычный модуль:

export function run() {
  console.log('CLI');
}

Для превращения его в исполняемый инструмент можно использовать banner:

await build({
  entryPoints: ['src/index.js'],
  outfile: 'dist/cli.js',
  bundle: true,
  platform: 'node',
  banner: {
    js: '#!/usr/bin/env node'
  }
});

После этого файл становится пригодным для регистрации в поле bin.


Распространённые ошибки

Добавление текста перед shebang

Некорректно:

banner: {
  js: `// comment
#!/usr/bin/env node`
}

Результат:

// comment
#!/usr/bin/env node

Операционная система не распознает такой файл как исполняемый скрипт, поскольку shebang обязан находиться в первой строке.

Правильно:

banner: {
  js: `#!/usr/bin/env node
// comment`
}

Потеря shebang после промежуточной обработки

Некоторые инструменты постобработки могут удалять первую строку файла.

Типичные признаки:

env: node: No such file or directory

или

command not found

После интеграции дополнительных инструментов рекомендуется проверять начало результирующего файла.


Использование Windows-путей

Некорректно:

#!C:\Program Files\nodejs\node.exe

Такой вариант не является переносимым.

Предпочтительный вариант:

#!/usr/bin/env node

Ошибка:

footer: {
  js: '#!/usr/bin/env node'
}

Результат:

// код приложения

#!/usr/bin/env node

Shebang в конце файла не имеет никакого смысла и будет проигнорирован.


Практический шаблон сборки CLI-приложения

import { build } from 'esbuild';

await build({
  entryPoints: ['src/cli.js'],
  outfile: 'dist/cli.js',
  bundle: true,
  platform: 'node',
  format: 'esm',
  minify: true,
  banner: {
    js: `#!/usr/bin/env node
/**
 * My CLI Tool
 * Production Build
 */`
  }
});

Особенности такой конфигурации:

  • создаётся единый бандл;
  • файл готов к публикации через npm;
  • shebang располагается в первой строке;
  • присутствует служебный заголовок;
  • код минимизируется;
  • результат может запускаться как самостоятельная консольная команда.

Комбинация shebang + banner является стандартным механизмом подготовки CLI-приложений в Esbuild. Shebang обеспечивает корректный запуск через Node.js, а banner позволяет внедрять лицензии, сведения о версии, комментарии сборки и другие служебные данные непосредственно в начало итогового файла без изменения исходного кода.