13 KiB
13 KiB
Spec: templates
Requirements
Package identity
- REQ-TPL-001: The package MUST be published as
@suaveplan/templatesunderpackages/universal/templates. - REQ-TPL-002: The package MUST have
"sideEffects": falseinpackage.json. - REQ-TPL-003: The package MUST expose subpath exports per feature module for tree-shaking:
./engine/parse-state,./engine/template-engine,./plugins/variables/dot-notation-plugin,./plugins/variables/array-index-plugin,./plugins/transform/case-transform-plugin,./plugins/transform/regex-plugin,./plugins/transform/string-split-plugin,./plugins/dynamic/counter-plugin,./plugins/dynamic/datetime-plugin,./plugins/dynamic/uuid-plugin,./plugins/control/conditional-plugin,./plugins/control/loop-plugin,./presets/minimal,./presets/standard,./presets/full,./utils/escape,./utils/format,./utils/get-nested-value,./errors. - REQ-TPL-004: The package MUST have zero browser, Node.js, or React runtime dependencies — it MUST run in Bun, Node.js, and browser environments without modification.
Dependencies
- REQ-TPL-010: Runtime dependencies MUST be limited to:
@suaveplan/error(workspace:^),@suaveplan/types(workspace:^),@suaveplan/logger(workspace:^),@opentelemetry/api(catalog:observability),zod(catalog:forms),tslib(catalog:build). - REQ-TPL-011: All internal workspace dependencies MUST use the
workspace:^specifier. - REQ-TPL-012: All external dependencies MUST be pinned via Bun catalog entries — no floating version ranges.
Error types
- REQ-TPL-020: A
TemplateErrorclass MUST extendSuaveplanErrorfrom@suaveplan/errorand serve as the base for all package errors. - REQ-TPL-021: A
PluginErrorclass MUST extendTemplateErrorand carrypluginName: stringandmatch: stringfields. - REQ-TPL-022: A
MaxDepthErrorclass MUST extendTemplateErrorand carrydepth: numberandmaxDepth: numberfields. - REQ-TPL-023: All error codes MUST be registered via
defineCodesfrom@suaveplan/errorunder theTEMPLATEnamespace:TEMPLATE.PARSE_ERROR,TEMPLATE.MISSING_VARIABLE,TEMPLATE.PLUGIN_CONFLICT,TEMPLATE.RENDER_ERROR,TEMPLATE.PLUGIN_ERROR,TEMPLATE.MAX_DEPTH. - REQ-TPL-024: Error code registration MUST be side-effect-imported from
src/index.tsso codes are always registered when the package is loaded.
Types
- REQ-TPL-030:
TemplateContextMUST be exported asRecord<string, unknown>. - REQ-TPL-031:
PluginPriorityMUST be exported as a numeric enum with valuesCRITICAL = 95,HIGH = 80,MEDIUM = 50,LOW = 20,MINIMAL = 5. - REQ-TPL-032:
ParseStateMUST be an interface with fields:counters: Map<string, number>,depth: number,maxDepth: number,cache: Map<string, unknown>,custom: Map<string, unknown>. - REQ-TPL-033:
PluginMatchMUST be an interface with fields:match: string,groups: RegExpMatchArray | null,index: number. - REQ-TPL-034:
PluginResultMUST bestring | Promise<string> | typeof SKIP_PLUGIN. - REQ-TPL-035:
SKIP_PLUGINMUST be exported as asymbolvalue; returning it fromPlugin.transformsignals the engine to leave the matched text unchanged. - REQ-TPL-036:
Plugin<TContext>MUST be an interface with:name: string,priority: number,pattern: RegExp,async?: boolean,transform(match, context, state): PluginResult,init?(context, state): void | Promise<void>,cleanup?(context, state): void | Promise<void>. - REQ-TPL-037:
PluginPreset<TContext>MUST be an interface with:name: string,plugins: Plugin<TContext>[]. - REQ-TPL-038:
TemplateEngineOptionsMUST be an interface with:maxDepth?: number,throwOnError?: boolean,enableCache?: boolean,onError?: (error: Error, template: string, context: TemplateContext) => void. - REQ-TPL-039:
ParseOptionsMUST be an interface with:maxDepth?: number,async?: boolean. - REQ-TPL-040:
CompiledTemplate<TContext>MUST be an interface with:source: string,parse(context: TContext): string,parseAsync(context: TContext): Promise<string>.
Zod schemas
- REQ-TPL-041:
TemplateEngineOptionsSchemaMUST be exported and validateTemplateEngineOptionswith Zod; all fields optional;maxDepthmust be a positive integer;throwOnErrorandenableCachemust be booleans;onErrormust be a function. - REQ-TPL-042:
ParseOptionsSchemaMUST be exported and validateParseOptionswith Zod. - REQ-TPL-043:
PluginPrioritySchemaMUST be exported and validate an integer in the range[0, 100].
ParseState
- REQ-TPL-050:
createParseState(maxDepth?: number)MUST return aParseStatewith all maps empty,depth = 0, andmaxDepthdefaulting to10. - REQ-TPL-051:
cloneParseState(state: ParseState)MUST return a structurally independent deep copy; mutations to the clone MUST NOT affect the original. - REQ-TPL-052:
clearCounters(state: ParseState)MUST reset all entries instate.countersto zero without removing keys. - REQ-TPL-053:
getCounter(state: ParseState, name: string)MUST return the current counter value or0if the counter has never been set. - REQ-TPL-054:
resetCounter(state: ParseState, name: string)MUST set a single named counter to zero.
TemplateEngine
- REQ-TPL-060:
TemplateEngine<TContext>MUST be generic overTContext extends TemplateContext. - REQ-TPL-061: The constructor MUST validate options via
TemplateEngineOptionsSchema; invalid options MUST throw aTemplateErrorwith codeTEMPLATE.PARSE_ERROR. - REQ-TPL-062:
.use(plugin)MUST append the plugin, detect name conflicts (samenamealready registered), re-sort all plugins byprioritydescending, and returnthisfor chaining. Name conflicts MUST throwTemplateErrorwith codeTEMPLATE.PLUGIN_CONFLICT. - REQ-TPL-063:
.usePreset(preset)MUST call.use()for each plugin inpreset.pluginsand returnthis. - REQ-TPL-064:
.remove(name)MUST remove the plugin with the given name and returnthis; silently no-ops if not found. - REQ-TPL-065:
.getPlugins()MUST return a readonly snapshot (not a live reference to the internal array). - REQ-TPL-066:
.parse(template, context, options?)MUST callinithooks in priority order, apply plugins in priority order viaparseInternal, callcleanuphooks in priority order, and return the rendered string. Sync plugins that return aPromisefrominitorcleanupMUST cause aTemplateErrorinstructing the caller to useparseAsync. - REQ-TPL-067:
.parseAsync(template, context, options?)MUST be the async variant of.parse(), awaiting all hooks and transforms. - REQ-TPL-068:
.compile(template)MUST return aCompiledTemplate<TContext>whose.parse()and.parseAsync()delegate to the engine. - REQ-TPL-069: When
throwOnErrorisfalse, errors during parse MUST be passed toonErrorand the original template string MUST be returned unchanged. - REQ-TPL-070: The internal depth guard MUST throw
MaxDepthError(codeTEMPLATE.MAX_DEPTH) whenstate.depth >= state.maxDepthbefore entering a new parse level. - REQ-TPL-071: Plugin matches MUST be applied in reverse-index order so earlier matches are not displaced by later replacements within the same pass.
Variable resolution plugins
- REQ-TPL-080:
createDotNotationPlugin()MUST match{path.to.value}patterns usinggetNestedValue; priority MUST bePluginPriority.LOW; MUST returnSKIP_PLUGINwhen the path is not found in context. - REQ-TPL-081:
createArrayIndexPlugin()MUST match{name[index]}patterns with zero-based indexing; priority MUST bePluginPriority.LOW; MUST returnSKIP_PLUGINwhen the array or index is absent.
Transform plugins
- REQ-TPL-090:
createCaseTransformPlugin()MUST support|upper,|lower,|title,|camel,|snake,|kebabtransform suffixes; priority MUST bePluginPriority.MEDIUM. - REQ-TPL-091:
createRegexPlugin(patterns)MUST accept an array of{ pattern: RegExp; replacement: string | ((match: string) => string) }entries applied in order; priority MUST bePluginPriority.MEDIUM. - REQ-TPL-092:
createStringSplitPlugin()MUST match{value|split:sep:index}and return the element at the given index after splitting onsep; out-of-range indexes MUST return an empty string; priority MUST bePluginPriority.MEDIUM.
Dynamic plugins
- REQ-TPL-100:
createCounterPlugin()MUST match{_counter:name:start:pad}, defaultstartto1andpadto0, increment on each match within theParseState, and zero-pad topadwidth; priority MUST bePluginPriority.HIGH. - REQ-TPL-101:
createDateTimePlugin(opts?)MUST accept{ clock?: Clock; defaultPattern?: string }, resolve the current time via the injectedClockorDate.now()when absent, format withformatDate, and default the pattern toYYYY-MM-DD; priority MUST bePluginPriority.HIGH. - REQ-TPL-102:
createUuidPlugin(opts?)MUST accept{ rng?: Rng }, generate UUID v4 for{_uuid}and nano ID for{_nanoid:length}using the injectedRngorcrypto.randomUUID/Math.randomwhen absent; priority MUST bePluginPriority.HIGH.
Control flow plugins
- REQ-TPL-110:
createConditionalPlugin()MUST match{condition}...{/condition}blocks, resolve the condition key (including dot-notation paths) from context, keep the block when truthy, remove it when falsy; priority MUST bePluginPriority.CRITICAL. - REQ-TPL-111:
createLoopPlugin()MUST match{loop:items}...{/loop}blocks, resolveitemsto an array in context, render the block body once per element with the element accessible as{item}, and produce an empty string for missing or non-array values; priority MUST bePluginPriority.CRITICAL.
Presets
- REQ-TPL-120:
minimalPreset()MUST return aPluginPresetcontaining exactly:dot-notation,array-index. - REQ-TPL-121:
standardPreset()MUST return aPluginPresetcontaining exactly:dot-notation,array-index,case-transform,counter,datetime,uuid. - REQ-TPL-122:
fullPreset()MUST return aPluginPresetcontaining all standard plugins plus:regex,string-split,conditional. - REQ-TPL-123:
fullPresetWithLoops()MUST return aPluginPresetcontaining all full plugins plus:loop. - REQ-TPL-124: Each call to a preset factory MUST return new plugin instances — presets MUST NOT share mutable state between engine instances.
Utilities
- REQ-TPL-130:
escapeRegex(str)MUST escape all regex metacharacters so the result is safe to use innew RegExp(...). - REQ-TPL-131:
isEscaped(template, index)MUST returntrueif the character atindexis preceded by a backslash escape. - REQ-TPL-132:
unescapeTemplate(str)MUST remove backslash-escape characters from a template string. - REQ-TPL-133:
formatDate(date, pattern)MUST replace tokensYYYY,MM,DD,HH,mm,ss,SSSwith the corresponding UTC-or-local date components zero-padded to their natural widths. - REQ-TPL-134:
generateUuid(rng?)MUST produce a valid UUID v4 string; whenrngis injected it MUST be used for all random bytes. - REQ-TPL-135:
generateNanoid(length?, rng?)MUST produce a URL-safe ID of the given length (default 21) using only[0-9A-Za-z]characters; whenrngis injected it MUST be used for all random bytes. - REQ-TPL-136:
pad(value, length, char?, left?)MUST padvaluetolengthusingchar(default'0') on the left (default) or right. - REQ-TPL-137:
getNestedValue(obj, path)MUST traverse a plain object by dot-separated path segments and returnundefinedwhen any segment is absent. - REQ-TPL-138:
hasNestedPath(obj, path)MUST returntrueif and only if the final segment of the path exists and is notundefined. - REQ-TPL-139:
setNestedValue(obj, path, value)MUST create intermediate plain objects as needed and set the value at the final path segment.
OTel instrumentation
- REQ-TPL-140: A counter named
template.render.totalMUST be incremented once per.parse()/.parseAsync()call with anoutcomeattribute of"success"or"error". - REQ-TPL-141: A histogram named
template.render.duration_msMUST record the elapsed milliseconds for every.parse()/.parseAsync()call. - REQ-TPL-142: A counter named
template.plugin.calls.totalMUST be incremented once per plugin invocation with attributesplugin_name(string) andoutcome("success"|"skip"|"error"). - REQ-TPL-143: All OTel instruments MUST be no-op when no exporter is registered — the package MUST NOT fail or warn in environments without OTel.
Quality gates
- REQ-TPL-150: Test coverage MUST reach 100 statements / 100 branches / 100 functions / 100 lines measured by istanbul.
- REQ-TPL-151: Docs coverage MUST reach ≥ 90% as measured by
bun run docs:check. - REQ-TPL-152: Every implementation
.tsfile MUST have a sibling.mdwith ≥ 200 words, a basic example, an advanced example, and a full API reference section. - REQ-TPL-153:
bunx turbo lint typecheck test --filter=@suaveplan/templatesMUST be green before publish. - REQ-TPL-154: Property-based tests using fast-check MUST cover: idempotent rendering, counter monotone increase, and
getNestedValue/setNestedValueround-trip. - REQ-TPL-155: Benchmarks using mitata MUST cover: 100-variable dot-notation render (10 000 iterations), single no-op plugin overhead (100 000 iterations),
formatDateformatting (100 000 iterations), andgetNestedValue5-level deep (100 000 iterations). - REQ-TPL-156: The package MUST pass a tree-shake guard test confirming
"sideEffects": falseand that side-effect-only imports (error code registration) do not prevent dead-code elimination of unused modules.