Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
/**
* Dependency-graph smoke test.
*
* Verifies that the package can be loaded and that the core exports resolve
* to the expected types without import errors. This acts as a runtime
* canary for tree-shaking and circular-dependency regressions.
*/
import { describe, expect, it } from "@suaveplan/testing/runner";
describe("dep-graph", () => {
it("imports TemplateEngine without errors", {
timeout: 30000,
}, async () => {
const { TemplateEngine } = await import(
"../src/engine/TemplateEngine.js"
);
expect(typeof TemplateEngine).toBe("function");
});
it("imports all error classes", { timeout: 30000 }, async () => {
const { TemplateError, PluginError, MaxDepthError } = await import(
"../src/errors/template-error.js"
);
expect(typeof TemplateError).toBe("function");
expect(typeof PluginError).toBe("function");
expect(typeof MaxDepthError).toBe("function");
});
it("imports all preset factories", { timeout: 30000 }, async () => {
const { minimalPreset } = await import("../src/presets/minimal.js");
const { standardPreset } = await import("../src/presets/standard.js");
const { fullPreset, fullPresetWithLoops } = await import(
"../src/presets/full.js"
);
expect(typeof minimalPreset).toBe("function");
expect(typeof standardPreset).toBe("function");
expect(typeof fullPreset).toBe("function");
expect(typeof fullPresetWithLoops).toBe("function");
});
it("imports all plugin factories", { timeout: 30000 }, async () => {
const { createDotNotationPlugin } = await import(
"../src/plugins/variables/DotNotationPlugin.js"
);
const { createArrayIndexPlugin } = await import(
"../src/plugins/variables/ArrayIndexPlugin.js"
);
const { createCaseTransformPlugin } = await import(
"../src/plugins/transform/CaseTransformPlugin.js"
);
const { createRegexPlugin } = await import(
"../src/plugins/transform/RegexPlugin.js"
);
const { createStringSplitPlugin } = await import(
"../src/plugins/transform/StringSplitPlugin.js"
);
const { createCounterPlugin } = await import(
"../src/plugins/dynamic/CounterPlugin.js"
);
const { createDateTimePlugin } = await import(
"../src/plugins/dynamic/DateTimePlugin.js"
);
const { createUuidPlugin } = await import(
"../src/plugins/dynamic/UuidPlugin.js"
);
const { createConditionalPlugin } = await import(
"../src/plugins/control/ConditionalPlugin.js"
);
const { createLoopPlugin } = await import(
"../src/plugins/control/LoopPlugin.js"
);
for (const factory of [
createDotNotationPlugin,
createArrayIndexPlugin,
createCaseTransformPlugin,
createRegexPlugin,
createStringSplitPlugin,
createCounterPlugin,
createDateTimePlugin,
createUuidPlugin,
createConditionalPlugin,
createLoopPlugin,
]) {
expect(typeof factory).toBe("function");
}
});
it("imports all utility functions", { timeout: 30000 }, async () => {
const escapeMod = await import("../src/utils/escape.js");
const format = await import("../src/utils/format.js");
const nested = await import("../src/utils/nested-value.js");
expect(typeof escapeMod.escapeRegex).toBe("function");
expect(typeof escapeMod.isEscaped).toBe("function");
expect(typeof escapeMod.unescapeTemplate).toBe("function");
expect(typeof format.pad).toBe("function");
expect(typeof format.formatDate).toBe("function");
expect(typeof format.generateUuid).toBe("function");
expect(typeof format.generateNanoid).toBe("function");
expect(typeof nested.getNestedValue).toBe("function");
expect(typeof nested.hasNestedPath).toBe("function");
expect(typeof nested.setNestedValue).toBe("function");
});
it("SKIP_PLUGIN is a symbol", { timeout: 30000 }, async () => {
const { SKIP_PLUGIN } = await import("../src/types/index.js");
expect(typeof SKIP_PLUGIN).toBe("symbol");
});
it("PluginPriority enum has correct ordering", {
timeout: 30000,
}, async () => {
const { PluginPriority } = await import("../src/types/index.js");
expect(PluginPriority.MINIMAL).toBeLessThan(PluginPriority.LOW);
expect(PluginPriority.LOW).toBeLessThan(PluginPriority.MEDIUM);
expect(PluginPriority.MEDIUM).toBeLessThan(PluginPriority.HIGH);
expect(PluginPriority.HIGH).toBeLessThan(PluginPriority.CRITICAL);
});
});

View File

@@ -0,0 +1,67 @@
/**
* No-disallowed audit: verify that non-deterministic global APIs
* (`Date`, `Math.random`, `crypto.randomUUID`) are only used in the
* approved files listed below and never leak into business-logic modules.
*
* Approved list (files that legitimately need these APIs):
* - src/utils/format.ts (generateUuid, generateNanoid, formatDate)
* - src/plugins/dynamic/DateTimePlugin.ts (uses Date via clock fallback)
* - src/plugins/dynamic/UuidPlugin.ts (delegates to format.ts)
* - src/base/suaveplan-error.ts (error timestamp)
*/
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
const SRC_ROOT = join(import.meta.dirname ?? __dirname, "../src");
const APPROVED = new Set([
join(SRC_ROOT, "utils/format.ts"),
join(SRC_ROOT, "plugins/dynamic/DateTimePlugin.ts"),
join(SRC_ROOT, "plugins/dynamic/UuidPlugin.ts"),
join(SRC_ROOT, "otel/templates-otel.ts"),
]);
// Patterns that indicate direct usage of non-deterministic globals
const DISALLOWED = [
/\bDate\.now\b/,
/\bnew Date\(\)/,
/\bMath\.random\b/,
/\bcrypto\.randomUUID\b/,
];
function collectTs(dir: string): string[] {
const results: string[] = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
const stat = statSync(full);
if (stat.isDirectory()) {
results.push(...collectTs(full));
} else if (
entry.endsWith(".ts") &&
!entry.endsWith(".test.ts") &&
!entry.endsWith(".bench.ts")
) {
results.push(full);
}
}
return results;
}
describe("no-disallowed globals in non-approved files", () => {
const files = collectTs(SRC_ROOT).filter((f) => !APPROVED.has(f));
for (const file of files) {
it(`${file.replace(SRC_ROOT, "src")} has no direct Date/Math.random usage`, {
timeout: 5000,
}, () => {
const src = readFileSync(file, "utf-8");
for (const pattern of DISALLOWED) {
expect(
pattern.test(src),
`Found disallowed pattern ${pattern} in ${file}`
).toBe(false);
}
});
}
});

View File

@@ -0,0 +1,102 @@
/**
* Integration tests for all preset bundles.
*/
import { describe, expect, it } from "@suaveplan/testing/runner";
import type { createParseState } from "../src/engine/ParseState.js";
import { TemplateEngine } from "../src/engine/TemplateEngine.js";
import { fullPreset, fullPresetWithLoops } from "../src/presets/full.js";
import { minimalPreset } from "../src/presets/minimal.js";
import { standardPreset } from "../src/presets/standard.js";
type Ctx = Record<string, unknown>;
describe("minimalPreset", () => {
it("resolves dot-notation", { timeout: 5000 }, () => {
const e = new TemplateEngine<Ctx>().usePreset(minimalPreset<Ctx>());
expect(e.parse("{user.name}", { user: { name: "Alice" } })).toBe(
"Alice"
);
});
it("resolves array index", { timeout: 5000 }, () => {
const e = new TemplateEngine<Ctx>().usePreset(minimalPreset<Ctx>());
expect(e.parse("{items[1]}", { items: ["a", "b"] })).toBe("b");
});
it("has 2 plugins", { timeout: 5000 }, () => {
const e = new TemplateEngine<Ctx>().usePreset(minimalPreset<Ctx>());
expect(e.getPlugins()).toHaveLength(2);
});
});
describe("standardPreset", () => {
it("resolves variables, counter, case transform, conditional", {
timeout: 5000,
}, () => {
const e = new TemplateEngine<Ctx>().usePreset(standardPreset<Ctx>());
const result = e.parse(
"{if:active}Hello {name:title}! #{_counter}{/if}",
{ active: true, name: "john doe" }
);
expect(result).toBe("Hello John Doe! #1");
});
it("generates UUID in standard preset", { timeout: 5000 }, () => {
const e = new TemplateEngine<Ctx>().usePreset(standardPreset<Ctx>());
const result = e.parse("{_uuid}", {});
expect(result).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
);
});
});
describe("fullPreset", () => {
it("includes regex and string-split", { timeout: 5000 }, () => {
const e = new TemplateEngine<Ctx>().usePreset(fullPreset<Ctx>());
expect(
e.parse("{phone:/\\D/g:replace:}", { phone: "(123) 456-7890" })
).toBe("1234567890");
expect(e.parse("{email:@[0]}", { email: "user@example.com" })).toBe(
"user"
);
});
});
describe("fullPresetWithLoops", () => {
it("iterates array with loop plugin", { timeout: 5000 }, () => {
const e = new TemplateEngine<Ctx>();
const preset = fullPresetWithLoops<Ctx>((tpl, ctx, state) =>
(
e as unknown as {
parseInternal: (
t: string,
c: Ctx,
s: ReturnType<typeof createParseState>
) => string;
}
).parseInternal(tpl, ctx, state)
);
e.usePreset(preset);
const result = e.parse("{each:items}{item} {/each}", {
items: ["X", "Y", "Z"],
});
expect(result).toBe("X Y Z ");
});
it("preset name is full-with-loops", { timeout: 5000 }, () => {
const preset = fullPresetWithLoops<Ctx>((t, c) => t);
expect(preset.name).toBe("full-with-loops");
});
});
describe("preset registration idempotency", () => {
it("applying same preset twice does not duplicate plugins", {
timeout: 5000,
}, () => {
const e = new TemplateEngine<Ctx>();
e.usePreset(minimalPreset<Ctx>());
const countAfterFirst = e.getPlugins().length;
e.usePreset(minimalPreset<Ctx>());
expect(e.getPlugins()).toHaveLength(countAfterFirst);
});
});

View File

@@ -0,0 +1,107 @@
/**
* Property-based tests using fast-check.
*
* Covers:
* - Idempotent rendering: re-parsing an already-rendered literal produces same output.
* - Counter monotone: named counters increase within a single parse call.
* - getNestedValue / setNestedValue round-trip.
* - PluginPriority strict ordering.
*/
import { describe, expect, it } from "@suaveplan/testing/runner";
import * as fc from "fast-check";
import { TemplateEngine } from "../src/engine/TemplateEngine.js";
import { createCounterPlugin } from "../src/plugins/dynamic/CounterPlugin.js";
import { createDotNotationPlugin } from "../src/plugins/variables/DotNotationPlugin.js";
import { PluginPriority } from "../src/types/index.js";
import { getNestedValue, setNestedValue } from "../src/utils/nested-value.js";
describe("idempotent literal rendering", () => {
it("re-parsing a rendered literal string returns the same string", {
timeout: 30000,
}, () => {
const e = new TemplateEngine().use(createDotNotationPlugin());
fc.assert(
fc.property(
// Strings with no { or } — guaranteed literals after render
fc.string({
unit: fc.char().filter((c) => c !== "{" && c !== "}"),
}),
(literal) => {
const first = e.parse(literal, {});
const second = e.parse(first, {});
expect(second).toBe(first);
}
),
{ numRuns: 200 }
);
});
});
describe("counter monotone within single parse", () => {
it("counter values strictly increase left-to-right in one parse call", {
timeout: 30000,
}, () => {
fc.assert(
fc.property(fc.integer({ min: 2, max: 10 }), (count) => {
const e = new TemplateEngine().use(createCounterPlugin());
// Build a template with `count` counter tokens
const template = Array.from(
{ length: count },
() => "{_counter}"
).join(" ");
const result = e.parse(template, {});
const values = result
.split(" ")
.map((v) => Number.parseInt(v, 10));
for (let i = 1; i < values.length; i++) {
expect(values[i]).toBeGreaterThan(values[i - 1] as number);
}
}),
{ numRuns: 100 }
);
});
});
describe("getNestedValue / setNestedValue round-trip", () => {
it("reading back after set returns the same primitive", {
timeout: 30000,
}, () => {
// Safe path: only lowercase letters and digits, no array notation
const safePath = fc
.array(fc.stringMatching(/^[a-z][a-z0-9]{0,5}$/), {
minLength: 1,
maxLength: 4,
})
.map((parts) => parts.join("."));
fc.assert(
fc.property(
safePath,
fc.oneof(
fc.integer(),
fc.string(),
fc.boolean()
// Note: null excluded because getNestedValue returns undefined for null values
),
(path, value) => {
const obj: Record<string, unknown> = {};
setNestedValue(obj, path, value);
const retrieved = getNestedValue(obj, path);
expect(retrieved).toBe(value);
}
),
{ numRuns: 300 }
);
});
});
describe("PluginPriority strict ordering", () => {
it("MINIMAL < LOW < MEDIUM < HIGH < CRITICAL", { timeout: 5000 }, () => {
expect(PluginPriority.MINIMAL).toBeLessThan(PluginPriority.LOW);
expect(PluginPriority.LOW).toBeLessThan(PluginPriority.MEDIUM);
expect(PluginPriority.MEDIUM).toBeLessThan(PluginPriority.HIGH);
expect(PluginPriority.HIGH).toBeLessThan(PluginPriority.CRITICAL);
});
});

View File

@@ -0,0 +1,8 @@
// Ensure `globalThis.crypto` is available in bare-Node vitest runs.
import { webcrypto } from "node:crypto";
type WritableGlobal = { crypto?: Crypto };
const g = globalThis as WritableGlobal;
if (g.crypto === undefined) {
g.crypto = webcrypto as unknown as Crypto;
}

View File

@@ -0,0 +1,69 @@
/**
* Tree-shake smoke test.
*
* Verifies that each sub-module can be imported in isolation without pulling
* in unrelated code. This acts as a proxy for correct `sideEffects: false`
* behaviour and clean ESM boundaries.
*/
import { describe, expect, it } from "@suaveplan/testing/runner";
describe("tree-shake isolation", () => {
it("can import only escape utils", { timeout: 30000 }, async () => {
const mod = await import("../src/utils/escape.js");
expect(typeof mod.escapeRegex).toBe("function");
expect(typeof mod.isEscaped).toBe("function");
expect(typeof mod.unescapeTemplate).toBe("function");
});
it("can import only format utils", { timeout: 30000 }, async () => {
const mod = await import("../src/utils/format.js");
expect(typeof mod.pad).toBe("function");
expect(typeof mod.formatDate).toBe("function");
expect(typeof mod.generateUuid).toBe("function");
expect(typeof mod.generateNanoid).toBe("function");
});
it("can import only nested-value utils", { timeout: 30000 }, async () => {
const mod = await import("../src/utils/nested-value.js");
expect(typeof mod.getNestedValue).toBe("function");
expect(typeof mod.hasNestedPath).toBe("function");
expect(typeof mod.setNestedValue).toBe("function");
});
it("can import ParseState without TemplateEngine", {
timeout: 30000,
}, async () => {
const mod = await import("../src/engine/ParseState.js");
expect(typeof mod.createParseState).toBe("function");
expect(typeof mod.cloneParseState).toBe("function");
});
it("can import a single plugin without loading all others", {
timeout: 30000,
}, async () => {
const mod = await import(
"../src/plugins/variables/DotNotationPlugin.js"
);
expect(typeof mod.createDotNotationPlugin).toBe("function");
});
it("can import schemas in isolation", { timeout: 30000 }, async () => {
const mod = await import("../src/schemas/templates.schemas.js");
expect(mod.TemplateEngineOptionsSchema).toBeDefined();
expect(mod.ParseOptionsSchema).toBeDefined();
expect(mod.PluginPrioritySchema).toBeDefined();
});
it("minimalPreset does not import dynamic plugins", {
timeout: 30000,
}, async () => {
// If minimalPreset somehow pulled in DateTimePlugin the import would
// still succeed, but this test documents the intended isolation.
const { minimalPreset } = await import("../src/presets/minimal.js");
const preset = minimalPreset();
expect(preset.plugins.map((p) => p.name)).toEqual(
expect.arrayContaining(["array-index", "dot-notation"])
);
expect(preset.plugins).toHaveLength(2);
});
});