DevBundler - Concatenates source files into one file, driven by comment directives - for languages and formats that have no module system of their own: TypeScript in global scope, HTML, CSS, SQL, shell scripts, configuration files.
Find a file
Michel Abele 1fbe0432bd Implement the bundler, tests, examples and packaging
dev-bundler concatenates source files into one file, driven by comment
  directives, for languages and formats without a module system of their
  own: TypeScript in global scope, HTML, CSS, SQL, shell scripts.

  The bundler works purely on the text level. A directive is any line that,
  apart from indentation and a comment marker, consists of nothing but
  @name [argument] - so the same @include works in every format that uses
  one of the known comment markers.

  - @include and @include_once insert files recursively, with cycle
    detection and the full include chain in every error message
  - @if / @elif / @else / @endif with flags from -D, negation, && , || and
    parentheses; conditions are checked for syntax even in dead branches,
    so a typo does not hide until someone sets the matching flag
  - inserted text is re-indented to the @include line without introducing
    trailing whitespace
  - CLI with --check for CI and --list-sources for makefiles and watchers
  - unknown @names pass through unchanged; only a close match to a real
    directive warns on stderr, so @param and @media stay harmless

  Covered by 99 tests, including two runnable example projects that are
  kept working by the suite.

  Packaged with hatchling for PyPI: dev-bundler entry point, PEP 639
  license metadata, py.typed marker, no runtime dependencies.

  Also corrects the copyright holder in LICENSE.
2026-08-10 08:59:02 +02:00
examples Implement the bundler, tests, examples and packaging 2026-08-10 08:59:02 +02:00
src/dev_bundler Implement the bundler, tests, examples and packaging 2026-08-10 08:59:02 +02:00
tests Implement the bundler, tests, examples and packaging 2026-08-10 08:59:02 +02:00
.gitattributes Implement the bundler, tests, examples and packaging 2026-08-10 08:59:02 +02:00
.gitignore Implement the bundler, tests, examples and packaging 2026-08-10 08:59:02 +02:00
LICENSE Implement the bundler, tests, examples and packaging 2026-08-10 08:59:02 +02:00
pyproject.toml Implement the bundler, tests, examples and packaging 2026-08-10 08:59:02 +02:00
README.md Implement the bundler, tests, examples and packaging 2026-08-10 08:59:02 +02:00

dev-bundler

Concatenates source files into one file, driven by comment directives — for languages and formats that have no module system of their own: TypeScript in global scope, HTML, CSS, SQL, shell scripts, configuration files.

The bundler works purely on the text level. It knows nothing about the grammar of the target language, only lines like these:

// app.ts
// @include ./lib/math.ts
// @include ./lib/dom.ts

main();
<!-- index.html -->
<body>
  <!-- @include ./partials/nav.html -->
</body>

That way one and the same bundler handles every file type, and the module structure lives where it belongs: in the source files.

Installation

pip install -e .

Afterwards dev-bundler is available as a command; python -m dev_bundler always works as an alternative. There are no runtime dependencies, Python ≥ 3.10 is enough.

Usage

dev-bundler src/app.ts -o dist/app.ts
dev-bundler src/app.ts -o dist/app.ts -D DEBUG
dev-bundler src/index.html -o dist/index.html --markers
dev-bundler src/app.ts                      # output on stdout
Option Effect
-o, --out PATH Target file; missing directories are created. Without it: stdout
-D, --define FLAG Set a flag for @if, may be repeated
--root DIR Base for includes with a leading / (default: directory of the entry file)
--markers Put a comment naming the source file before and after every include
--no-reindent Do not indent inserted text to match the @include line
--check Only check whether --out is up to date (exit code 1 if not) — for CI
--list-sources Print all files involved instead of the bundle — for makefiles and watchers
--max-depth N Maximum include depth (default: 32)
--encoding NAME Encoding of source and target files (default: utf-8)
--eol {lf,crlf} Line ending of the output (default: lf)
-q, --quiet Suppress notes on stderr

Exit codes: 0 success, 1 --check failed, 2 error while building.

Directives

A directive is a line that — apart from indentation and a comment marker — consists of nothing but @name [argument].

@include PATH

Inserts the contents of the file at this point, recursively. An @include always inserts, even repeatedly.

// @include ./lib/math.ts
// @include "./lib/with spaces.ts"
// @include /shared/util.ts        // relative to --root

Paths without a leading / are relative to the including file, not to the working directory. Circular includes abort with an error message showing the whole chain.

@include_once PATH

Like @include, but inserts the file only the first time per bundle — the counterpart to #pragma once. For shared base modules that several files need:

// lib/math.ts and lib/dom.ts both contain:
// @include_once ./log.ts

What counts is the resolved path, so ./lib/x.ts and ./lib/../lib/x.ts are considered the same file.

@if / @elif / @else / @endif

Emits a block only if the condition holds. Flags come from -D:

// @if DEBUG
console.log("dev build");
// @elif STAGING
console.log("staging");
// @else
// @endif

Conditions know flags, negation !, and &&, or || and parentheses:

// @if DEBUG && (LINUX || MACOS)
// @if !RELEASE

@if chains have to be closed within the same file. An @include in a branch that does not apply is not executed — the file is never even read. Conditions are still always checked for syntax, even in a dead branch.

Comment markers

Recognized are //, #, --, ;, %, ', *, REM, /* … */, <!-- … -->, {# … #} and {{! … }} — as well as directives without any comment marker at all, for formats that have no comments.

Lines with an unknown @name are not directives and pass through unchanged; otherwise every @param, @media or @Override in the source would be an error. So that typos do not slip through silently, the bundler warns on stderr when an unknown name closely resembles a directive:

dev-bundler: app.ts:1: @inclde is not a directive - did you mean @include?

Indentation

The inserted text is indented by the indentation of the @include line. Blank lines stay blank, no trailing whitespace is produced. For HTML that is the difference between readable and unreadable output:

<body>
  <!-- @include ./partials/nav.html --><body>
</body>                                            <nav>
                                                     <a>Home</a>
                                                   </nav>
                                                 </body>

With --no-reindent the text stays exactly as it is in the source file.

Examples

Under examples/ there are two working projects:

dev-bundler examples/typescript/src/app.ts -D DEBUG --markers
dev-bundler examples/html/src/index.html -D DEBUG

Integrating into a build

--list-sources prints all files involved, --check verifies without writing:

# Rebuild when something has changed
dev-bundler src/app.ts -o dist/app.ts

# Make sure in CI that dist/ matches the sources
dev-bundler src/app.ts -o dist/app.ts --check

# List all sources of a bundle, e.g. for inotifywait or make
dev-bundler src/app.ts --list-sources

As a library

from pathlib import Path
from dev_bundler import BundleOptions, bundle

result = bundle(
    Path("src/app.ts"),
    BundleOptions(defines=frozenset({"DEBUG"}), markers=True),
)
print(result.text)
print(result.sources)    # all files read, in order
print(result.warnings)   # notes that did not prevent the build

Errors arrive as dev_bundler.BundleError with file, line and include chain.

Development

python -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest

License

MIT — see LICENSE.