- Python 100%
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.
|
||
|---|---|---|
| examples | ||
| src/dev_bundler | ||
| tests | ||
| .gitattributes | ||
| .gitignore | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
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.