Design philosophy · Decision framework
Nift architectural rules
Nift is allowed to grow, but not in every direction. These rules turn the project philosophy into a practical review checklist for deciding what belongs in Nift, what should remain external, and what must stay true as the implementation evolves.
A new feature can absolutely be valuable. The question is whether it strengthens Nift's job as a small, dependency-aware build-time glue layer without quietly turning it into a package manager, browser runtime, compiler suite, shell scripting environment or general application framework.
1. Nift owns build-time composition, not the browser runtime.
Nift should decide what can be known while building and emit ordinary files. Runtime behaviour remains ordinary browser JavaScript, a framework island, a backend, a serverless function or any other runtime technology chosen by the project.
known at build time known only at runtime
------------------- ---------------------
JSON catalogue ── Nift signed-in user
navigation tree ── Nift live stock level
shared layout ── Nift websocket event
static table rows ── Nift payment result
Both can exist on the same page. This is why @json, @for and @if fit naturally: they render known data into static output. It is also why Nift does not need to invent a client component runtime merely because some pages contain interactive widgets.
2. Ordinary web technologies must remain ordinary.
HTML should look like HTML, CSS like CSS and JavaScript like JavaScript. Nift-specific syntax should solve project-aware build problems rather than wrap the entire web platform in a proprietary API.
A developer should be able to copy a normal CSS rule, a browser fetch() call, a Web Component or a React mount point into a Nift project without translating it into “the Nift way”.
3. Nift may perform optional final-output optimisation, but source-language compilation remains external.
This is the boundary that separates the embedded minifier from tools such as TypeScript, Sass, JSX compilers and bundlers.
TypeScript ──► JavaScript ──┐
Sass ──► CSS ──┼──► Nift composition ──► optional final minification
JSX tool ──► JavaScript ──┘
Nift does not need to become the source compiler. Final-output optimisation is different because it operates on the file Nift is already about to write. It can remain optional, format-aware and semantically conservative without making Nift responsible for another language's compilation model.
The experimental tscc project is therefore deliberately separate. It can be used beside Nift, distributed with Nift one day, or even invoked by a user's build script, without changing the architectural rule that TypeScript compilation is not part of Nift's template/build semantics.
4. The template language is a build language, not a general scripting language.
The language may gain concise constructs that directly describe static rendering, but it should resist features whose main purpose is arbitrary computation.
Fits naturally
Inputs, metadata, JSON loading, schema validation, loops, conditions, sorting, path resolution and dependency declaration.
Raises the bar
Mutable variables, arbitrary assignment, user functions, unrestricted arithmetic/state, shell execution and an embedded general-purpose runtime.
The useful distinction is not “simple versus powerful”. It is declarative rendering versus open-ended program execution.
5. Structured data flows into rendering; templates should not become a data-processing engine.
Nift can load JSON, validate it with JSON Schema, iterate collections, expose loop metadata, sort a collection for rendering and branch on values. Those operations directly answer “what output should this data produce?”
Full joins, grouping engines, arbitrary transformation pipelines and mutable document updates are a different architectural category. When data needs substantial preparation, a normal script/program can produce the JSON that Nift consumes.
API / database / script
│
▼
prepared JSON
│
▼
@json + @for + @if
│
▼
static output 6. Nift should compose with the ecosystem rather than recreate it through plugins.
The web ecosystem is part of Nift's ecosystem. npm packages, Bun, Vite, React, Vue, Svelte, Tailwind, image tools, databases, cloud CLIs, Terraform and backend languages already communicate through files, HTTP and processes.
Nift should not need an AWS plugin to deploy files to AWS, a React plugin to leave a React mount point in HTML, or a Sass plugin to consume CSS produced by Sass. Integration through standard boundaries is usually more durable than maintaining Nift-specific adapters for every tool.
7. Build orchestration belongs in build scripts, not template shell execution.
Nift templates should not execute arbitrary shell commands. Projects that need multiple build stages can use Make, npm scripts, shell scripts, CI workflows, task runners or any orchestration tool they already trust.
{
"scripts": {
"build:css": "sass src/app.scss build/app.css",
"build:ts": "tsc -p tsconfig.json",
"build:nift": "nift build",
"build": "npm run build:css && npm run build:ts && npm run build:nift"
}
} This is both more powerful and safer than allowing arbitrary command execution from markup being parsed for output.
8. Dependencies must be explicit enough to explain rebuilds.
Nift's incremental build model should always be able to answer why an output was rebuilt. Relationships discovered by @input, @json, templates and @dep are recorded rather than hidden in an opaque cache.
The distinction between dependencies and requirements should remain precise:
| Relationship | Meaning | Incremental consequence |
|---|---|---|
| dependency | Its content/state can affect generated bytes. | Modification can trigger rebuild. |
| requirement | The generated output assumes the target still exists. | Missing target triggers rebuild; modification alone does not. |
9. Incremental correctness outranks incremental cleverness.
A skipped build that should have happened is more serious than a conservative extra rebuild. Metadata, dependency state, requirements, minification settings and format-version changes must invalidate previous output when they make that output untrustworthy.
Performance work should optimise the evidence-gathering path without weakening the correctness contract.
10. Failed builds must not destroy the last known-good result.
Rendering, validation and optional minification should succeed before Nift commits new output/build metadata. A malformed JSON file, schema violation or minifier error should report a failure while leaving the previous successful output intact where practical.
This rule also applies to project mutations: validate first, then mutate persistent state.
11. Safe operations should be the default; destructive behaviour should be explicit.
Commands that can reasonably preserve user data should do so. The standalone minify command follows this rule:
nift minify app.js
# writes app.min.js; app.js is unchanged
nift minify --in-place app.js
# explicit destructive request The same principle informs tracking/path collision checks and non-destructive failure handling elsewhere in the CLI.
12. Project paths and derived outputs are security/correctness boundaries.
Tracked names, content paths, output paths and generated metadata paths must not silently escape their permitted roots or collide in ways that let one operation overwrite unrelated project state. Filesystem convenience never outranks path safety.
13. Concurrency may improve speed but must not change semantics.
Parallel builds should be observationally equivalent to serial builds. Parser/minifier state used per output should be local or otherwise concurrency-safe, and persistent project state should have an intentional commit/locking model.
14. Diagnostics are part of the architecture.
A build tool that knows the source file, tracked output and parser location should preserve that information in an error. “Failed to build” is not an adequate substitute for a filename, line, column, source context and concrete reason.
15. Configuration should express policy, not hide magic.
Features such as minify-exts, incremental mode and per-file minification overrides are explicit configuration. Nift should avoid silently inspecting a repository and deciding that a technology stack must be transformed in a particular way.
For example, discovering package.json should not cause Nift to start running npm, and finding a .ts file should not silently invoke a TypeScript compiler.
16. Reusable mechanisms should have clean extraction boundaries.
If a subsystem is useful beyond Nift, it should be possible to make it an independent library/tool without taking Nift's project engine with it. The minifier is now structured this way inside the repository:
minifier/
include/minify/Minify.h public API
src/ self-contained implementation
cli/ standalone command
tests/ standalone regression gates
Nift
└── consumes minifier public API The dependency direction matters: Nift may embed the minifier; the minifier must not depend on Nift's tracking database, template parser or project model.
17. Compatibility may be retained quietly, but legacy concepts should not shape the modern API.
A narrow alias can keep an older project working without making that alias part of new documentation. Compatibility exists to reduce migration pain, not to make every historical feature permanent or contagious.
18. Every permanent feature pays a permanent complexity tax.
A feature should solve a recurring problem, have understandable semantics, be testable at its boundaries and be explainable without expanding Nift's mental model disproportionately.
A useful review question is:
Does this make Nift better at the small build-time job it owns, or does it make Nift responsible for a neighbouring tool's entire problem domain?
19. Tests define contracts, not implementation accidents.
Regression tests should protect observable behaviour, safety properties and explicit architectural boundaries. If a test's assumption disagrees with the intended contract, the test should be corrected rather than freezing an accidental implementation detail forever.
20. The architecture should remain explainable on one page.
Nift can gain sophisticated internals—hashing, concurrency, schema validation, dependency metadata, conservative minification—but its conceptual flow should remain close to:
tracked item
+ content
+ template
+ structured build-time data
│
▼
dependency-aware composition
│
▼
optional final-output optimisation
│
▼
ordinary files If explaining a proposed feature requires redefining that flow, the feature deserves much stronger scrutiny.
How to use these rules
They are intended as a design review checklist rather than immutable law. A future feature can challenge a rule, but the trade-off should be explicit. That keeps Nift's evolution deliberate instead of allowing a sequence of individually reasonable additions to accidentally turn it back into the much larger system that the stripped rewrite intentionally moved away from.