Architecture: conservative scanners, format-specific contracts
Minify++'s architecture is built around one uncomfortable truth: removing a single character can change a program. The implementation therefore optimises only where it can maintain enough lexical context to explain why the removal is safe.
Independent project boundary
include/minify/Minify++.h public C++ API
src/Minify++.cpp format implementations
src/Json.h private JSON parser
cli/main.cpp standalone CLI
tests/ independent correctness gates Nift consumes the public header and implementation. Minify++ does not know about Nift tracking, templates, configuration or deployment.
No universal whitespace pass
A universal text minifier would be simpler and much less trustworthy. Each mode owns the grammar boundaries that matter to it: raw text and mixed content for HTML; strings/functions/custom properties for CSS; lexical slash/brace/template context for JavaScript; element/expression boundaries for JSX; structured parsing for JSON.
Why not a full JavaScript AST?
A complete AST would enable deeper optimisation, but it would also move Minify++ toward being a compiler. The current scanner approach keeps startup and deployment tiny and is compatible with Minify++'s final-output role. When syntax is ambiguous, the preferred answer is conservative output plus a regression—not increasingly aggressive guessing.
Idempotence
A useful property of a minifier is stability:
minify(minify(source)) == minify(source) The JSX corpus enforces byte-for-byte idempotence, and direct format tests cover the same principle where useful. Stability makes build caches and diffs more predictable and catches scanners that change interpretation after their own first rewrite.
Error and write boundary
Transformation happens before destination replacement. The default CLI writes a new sibling file; in-place mode is explicit. Complete output is prepared in a sibling temporary directory and committed afterward, with existing permission bits preserved. Symbolic-link destinations are rejected rather than followed or silently replaced.
Standalone and Nift-embedded source, tests and build contracts are checked through an explicit 20-file equality gate. The repository also provides repeatable ASan/UBSan, deterministic mutation, independent CSS semantic comparison and per-format performance/RSS entry points.
Whitespace is sometimes syntax
The production fuzz campaign exposed a shared risk across otherwise different scanners: removing whitespace can manufacture a new delimiter. CSS can gain /*, JSX-aware JavaScript can gain an opener beginning with <, and HTML/XML can gain comment or CDATA-like prefixes. Safe removal therefore asks a format-specific question: can the surviving left and right neighborhoods become adjacent without creating new syntax?
One whole-buffer transaction
The library computes a complete output string before reporting success. It does not stream a partially transformed document to the caller. The CLI adds a second transaction boundary around files: prepare a sibling candidate, preserve permissions where replacing a regular file, then commit or report failure.
Why the scanner architecture survived hardening
Adversarial testing found real defects, but the fixes remained localized to understandable lexical boundaries. That is evidence for the compact architecture—not proof that scanners are universally sufficient. If a future supported feature cannot be implemented conservatively without whole-program semantic knowledge, the right response may be to preserve it or leave it to another tool.
What belongs in Minify++?
More adversarial syntax support, diagnostics and final-format coverage can fit. Type checking, bundling, framework plugins, shell execution and source-language compilation generally do not.