purity-seal
v0.0.0
Published
Purity Seal enforces dependency rules between files:
Readme
Purity Seal enforces dependency rules between files:
- Classify files
filepath -> classification, usually by file extension. Classifiers are composable. - Define a
partial orderthat expresses dependency rules between file classifications. Partial orders may not contain cycles. - Create a checker from the classifier and partial order. Checkers input a
(dependent, dependency)string pair, and output anopinion. Checkers are composable. - Run the checker against a dependency graph, typically using a build system plugin.
Examples
- Dog food! Purity Seal validates itself.
- Enforce Onion Architecture
- Separate Business Logic from Library
- Isolate Runtime Platforms
- Multiple Classifiers
- Alternative Checkers
- Lua
Enforce Onion Architecture
graph BT;
http.ts --> pure.ts
state.ts --> pure.ts
http.state.ts --> state.ts
http.state.ts --> http.tsconst classify = PS.Classify.Extensions(["pure", "state", "http"])
const po = PS.PartialOrder.Make([
[["http.state", "state.http"], ["state", "http"], "pure"],
])
const check = PS.Pipe(
PS.Checker.Build(classify)(po),
PS.Plugin.Esbuild,
)Separate Business Logic from Library
graph LR;
domain.ts --> pure.ts
math.ts --> domain.tsconst classify = PS.Classify.Extensions(["pure", "math", "domain"])
const po = PS.PartialOrder.Make([
["math", "domain", "pure"],
])
const check = PS.Pipe(
PS.Checker.Build(classify)(po),
PS.Plugin.Esbuild,
)Isolate Runtime Platforms
graph BT;
Fetch.http.ts --> pure.ts
dom.ts --> pure.ts
worker.ts --> pure.ts
Assert.test.ts --> pure.ts
subgraph Builds
browser.ts
thread.ts
test.ts
end
browser.ts --> dom.ts
browser.ts --> Fetch.http.ts
thread.ts --> Fetch.http.ts
thread.ts --> worker.ts
test.ts --> pure.ts
test.ts --> Assert.test.tsconst classify = PS.Classify.Extensions([
"pure", "http", "dom", "worker", "test", "browser", "thread",
])
const po = PS.PartialOrder.Make([
["browser", ["http", "dom"], "pure"],
["thread", ["http", "worker"], "pure"],
["test", "pure"],
])
const check = PS.Pipe(
PS.Checker.Build(classify)(po),
PS.Plugin.Esbuild,
)Multiple Classifiers
Third party code may follow different conventions.
graph BT;
pure.ts --> Stats
http.ts --> pure.ts
http.ts --> HTTP
http.ts --> Framework.cache
dom.http --> http.ts
dom.http ---> Frameworkconst libStats = PS.Classify.SetWhen(
filepath => filepath.startsWith("node_modules/Stats"),
_filepath => "pure",
)
const libHttp = PS.Classify.SetWhen(
filepath => filepath === "node_modules/HTTP/index.ts",
_filepath => "http",
)
const libGlob = PS.Classify.SetWhen(
filepath => filepath.startsWith("node_modules/framework"),
filepath => filepath.includes("cache") ? "http" : "dom.http",
)
const classify = PS.Pipe(
PS.Classify.Extensions(["pure", "http", "dom"]),
PS.Classify.Catch(libStats),
PS.Classify.Catch(libHttp),
PS.Classify.Catch(libGlob),
)
const po = PS.PartialOrder.Make([
["dom.http", ["dom", "http"], "pure"],
])
const check = PS.Pipe(
PS.Checker.Build(classify)(po),
PS.Plugin.Esbuild,
)Alternative Checkers
A checker does not have to form an opinion for a given node in the dependency graph. Instead, you can chain multiple checkers in a pipeline, which terminates when a checker forms an opinion.
const whitelist = new Set(["Source/Legacy/Foo.ts"])
const allowWhitelist = PS.Checker.AsksWhen(
([x, y]) => whitelist.has(x) || whitelist.has(y),
PS.Checker.Allow(),
)
const classify = PS.Pipe(
PS.Classify.Extensions(["pure", "http"]),
)
const po = PS.PartialOrder.Make([
["http", "pure"],
])
const check = PS.Pipe(
PS.Checker.Build(classify)(po),
PS.Checker.Then(allowWhitelist),
PS.Plugin.Esbuild,
)Lua
graph BT;
Player.game.lua --> pure.lua
test.lua --> pure.luaThe lua tooling ecosystem is less advanced than TypeScript, so Purity Seal bundles luaparse to help extract a dependency graph.
const classify = PS.Classify.Extensions([
"pure", "game", "test",
])
const po = PS.PartialOrder.Make([
["game", "pure"],// Game engine
["test", "pure"],// CLI
])
const check = PS.Checker.Build(classify)(po)
const deps = await PS.Plugin.Lua.BuildDeps(
// Arg 1: list of entry point modules
// resolveModule: maps modules (including entries) to raw filepath.
["Main.game.lua", "test.lua"],
{ luaVersion: "5.1", resolveModule: x => "source/" + x },
)
// { deny: Deps[], warn: string[] }
const out = PS.Checker.Validate(check, deps)