@edgedev/template-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +332 -0
- package/dist/hydrateValues.d.ts +11 -0
- package/dist/hydrateValues.d.ts.map +1 -0
- package/dist/hydrateValues.js +568 -0
- package/dist/hydrateValues.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +351 -0
- package/dist/index.js.map +1 -0
- package/dist/kvIndexClient.d.ts +49 -0
- package/dist/kvIndexClient.d.ts.map +1 -0
- package/dist/kvIndexClient.js +256 -0
- package/dist/kvIndexClient.js.map +1 -0
- package/dist/templateConfig.d.ts +3 -0
- package/dist/templateConfig.d.ts.map +1 -0
- package/dist/templateConfig.js +23 -0
- package/dist/templateConfig.js.map +1 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/unoSsr.d.ts +21 -0
- package/dist/unoSsr.d.ts.map +1 -0
- package/dist/unoSsr.js +198 -0
- package/dist/unoSsr.js.map +1 -0
- package/package.json +34 -0
- package/src/hydrateValues.ts +753 -0
- package/src/index.ts +487 -0
- package/src/kvIndexClient.ts +374 -0
- package/src/templateConfig.ts +25 -0
- package/src/types.ts +54 -0
- package/src/unoSsr.ts +232 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# @edge/template-engine
|
|
2
|
+
|
|
3
|
+
Utility function with rendering logic. Feed it content, values, and meta data and receive HTML you can drop into any Vue (or non-Vue) surface.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @edge/template-engine
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import renderTemplate from '@edge/template-engine'
|
|
15
|
+
|
|
16
|
+
const html = renderTemplate(contentString, valueMap, metaDefinition)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Treat `valueMap` and `metaDefinition` as override objects: they selectively replace values/meta defined within the template, and any omitted parameter (or key) falls back to the template's embedded defaults.
|
|
20
|
+
|
|
21
|
+
### Full Page Rendering
|
|
22
|
+
|
|
23
|
+
Use `pageRender` when you need to hydrate multiple blocks, render their HTML, and collect the UnoCSS used across the entire page.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { pageRender } from '@edge/template-engine'
|
|
27
|
+
|
|
28
|
+
const theme = {
|
|
29
|
+
extend: {
|
|
30
|
+
colors: { brand: '#2563eb' },
|
|
31
|
+
fontFamily: { brand: ['Inter', 'sans-serif'] },
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { blocks, css } = await pageRender(
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
name: 'hero',
|
|
39
|
+
content: '<section class="bg-brand text-white">{{{#text {"field":"headline"} }}}</section>',
|
|
40
|
+
values: { headline: 'Launch in days, not months.' },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'listing',
|
|
44
|
+
content: `
|
|
45
|
+
<ul>
|
|
46
|
+
{{{#array {"field":"items","as":"item"} }}}
|
|
47
|
+
<li class="flex gap-2">
|
|
48
|
+
<span class="font-semibold">{{ item.title }}</span>
|
|
49
|
+
<span class="text-sm text-gray-500">{{ item.subtitle }}</span>
|
|
50
|
+
</li>
|
|
51
|
+
{{{/array}}}
|
|
52
|
+
</ul>
|
|
53
|
+
`,
|
|
54
|
+
meta: {
|
|
55
|
+
items: { schema: { title: 'text', subtitle: 'text' } },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
theme,
|
|
60
|
+
'<div class="hidden md:block fixed inset-0 pointer-events-none radial-gradient-mask"></div>',
|
|
61
|
+
{
|
|
62
|
+
uniqueKey: 'org-123:site-456',
|
|
63
|
+
clientOptions: {
|
|
64
|
+
binding: env?.MY_INDEX_KV,
|
|
65
|
+
accountId: process.env.CF_ACCOUNT_ID!,
|
|
66
|
+
namespaceId: process.env.CF_NAMESPACE_ID!,
|
|
67
|
+
apiToken: process.env.CF_API_TOKEN!,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
// blocks => [{ name: 'hero', html: '<section ...>...</section>' }, ...]
|
|
73
|
+
// css => aggregated UnoCSS needed for all rendered HTML + extraHtml
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- Each block is hydrated first using the provided hydrate options, then rendered via `renderTemplate`.
|
|
77
|
+
- `extraHtml` is optional helper markup that should also contribute to UnoCSS generation (e.g. layout shells).
|
|
78
|
+
- The returned `css` string is ready to inline or serve as critical CSS. Rendered blocks remain accessible individually if you need to target specific components.
|
|
79
|
+
|
|
80
|
+
The renderer supports:
|
|
81
|
+
|
|
82
|
+
- Root-level and nested `#array` / `#subarray` blocks with aliases
|
|
83
|
+
- Conditional `#if` / `#else` blocks
|
|
84
|
+
- Simple blocks (`#text`, `#image`, `#textarea`, `#richtext`)
|
|
85
|
+
- Schema-aware formatting for `number`, `integer`, `money`, and `richtext` types using the provided `meta`
|
|
86
|
+
|
|
87
|
+
See `src/index.ts` for the full list of helpers that mimic the original Vue component.
|
|
88
|
+
|
|
89
|
+
## Comprehensive Example
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const content = `
|
|
93
|
+
<section class="playbook">
|
|
94
|
+
<header>
|
|
95
|
+
<h1>{{{#text {"field":"title"} }}}</h1>
|
|
96
|
+
<p>{{{#textarea {"field":"summary"} }}}</p>
|
|
97
|
+
<img :src="{{{#image {"field":"heroImage"} }}}" alt="Hero" />
|
|
98
|
+
{{{#richtext {"field":"body"} }}}
|
|
99
|
+
</header>
|
|
100
|
+
|
|
101
|
+
<div class="cta">
|
|
102
|
+
<a href="{{{ {"field":"heroHref","value":"https://edge.co"} }}}">{{{ {"field":"heroCta","type":"text"} }}}</a>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<ul class="stats">
|
|
106
|
+
{{{#array {"field":"stats","as":"stat"} }}}
|
|
107
|
+
<li>
|
|
108
|
+
<span class="label">{{ stat.label }}</span>
|
|
109
|
+
<span class="value">{{ stat.value }}</span>
|
|
110
|
+
</li>
|
|
111
|
+
{{{/array}}}
|
|
112
|
+
</ul>
|
|
113
|
+
|
|
114
|
+
<section class="team">
|
|
115
|
+
{{{#array {"field":"team","as":"member","schema":{"budget":"money"}} }}}
|
|
116
|
+
<article>
|
|
117
|
+
<h3>{{ member.name }}</h3>
|
|
118
|
+
<p>{{ member.role }}</p>
|
|
119
|
+
{{{#if {"cond":"item.isLead == true"} }}}
|
|
120
|
+
<span class="badge">Lead</span>
|
|
121
|
+
{{{#else}}}
|
|
122
|
+
<span class="badge muted">Contributor</span>
|
|
123
|
+
{{{/if}}}
|
|
124
|
+
<p class="bio">{{ member.bio }}</p>
|
|
125
|
+
<div>Annual budget: {{ member.budget }}</div>
|
|
126
|
+
|
|
127
|
+
<ul>
|
|
128
|
+
{{{#subarray:project {"field":"item.projects","limit":2} }}}
|
|
129
|
+
<li>
|
|
130
|
+
<strong>{{ project.name }}</strong>
|
|
131
|
+
<span>{{ project.status }}</span>
|
|
132
|
+
</li>
|
|
133
|
+
{{{/subarray}}}
|
|
134
|
+
</ul>
|
|
135
|
+
</article>
|
|
136
|
+
{{{/array}}}
|
|
137
|
+
</section>
|
|
138
|
+
</section>
|
|
139
|
+
`
|
|
140
|
+
|
|
141
|
+
const values = {
|
|
142
|
+
title: 'Q4 Launch Playbook',
|
|
143
|
+
summary: 'Tactics and timelines for the winter product drop.',
|
|
144
|
+
heroImage: 'https://cdn.edge.co/playbook-hero.png',
|
|
145
|
+
heroHref: 'https://edge.co/campaigns/playbook',
|
|
146
|
+
heroCta: 'View Campaign Timeline',
|
|
147
|
+
body: '<p><strong>Note:</strong> This block accepts raw HTML.</p>',
|
|
148
|
+
stats: [
|
|
149
|
+
{ label: 'Net-new leads', value: 3400 },
|
|
150
|
+
{ label: 'Pipeline ($)', value: 1750000 },
|
|
151
|
+
],
|
|
152
|
+
team: [
|
|
153
|
+
{
|
|
154
|
+
name: 'Mia Chen',
|
|
155
|
+
role: 'Program Manager',
|
|
156
|
+
isLead: true,
|
|
157
|
+
budget: 125000,
|
|
158
|
+
bio: 'Owns launch roadmap & GTM alignment.',
|
|
159
|
+
projects: [
|
|
160
|
+
{ name: 'Hubble', status: 'Active' },
|
|
161
|
+
{ name: 'Lumen', status: 'Planning' },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'Jonas Patel',
|
|
166
|
+
role: 'Growth Engineer',
|
|
167
|
+
isLead: false,
|
|
168
|
+
budget: 60000,
|
|
169
|
+
bio: '<em>Automates activation experiments.</em>',
|
|
170
|
+
projects: [
|
|
171
|
+
{ name: 'Arcade', status: 'QA' },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const meta = {
|
|
178
|
+
stats: {
|
|
179
|
+
// object-based schema (field -> type)
|
|
180
|
+
schema: {
|
|
181
|
+
value: 'number',
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
team: {
|
|
185
|
+
// array-based schema (common on older Edge configs)
|
|
186
|
+
schema: [
|
|
187
|
+
{ field: 'budget', type: 'money' },
|
|
188
|
+
{ field: 'bio', type: 'richtext' },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const html = renderTemplate(content, values, meta)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Key behaviors demonstrated:
|
|
197
|
+
|
|
198
|
+
- `{{{#text}}}`, `{{{#textarea}}}`, `{{{#image}}}`, and `{{{#richtext}}}` show the simple block helpers.
|
|
199
|
+
- `{{ {"field":"...","type":"text"} }}` illustrates double-brace placeholders with type-aware escaping.
|
|
200
|
+
- `#array` handles the root `stats` and `team` lists, while `#subarray` reaches into each member’s nested `projects`.
|
|
201
|
+
- The `schema` on `stats` (object form) and `team` (array form) ensures `value`, `budget`, and `bio` pick up number/money/richtext formatting.
|
|
202
|
+
- The conditional `#if/#else` block switches the badge copy based on `item.isLead`.
|
|
203
|
+
- `limit` on `#subarray` trims the nested list to the first two entries.
|
|
204
|
+
|
|
205
|
+
## Cloudflare KV Index Client
|
|
206
|
+
|
|
207
|
+
The package also exposes a helper for working with Cloudflare KV index namespaces. Initialize it once with your credentials (and optional Worker KV binding) and reuse the client to run prefix queries.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { createKvIndexClient } from '@edge/template-engine'
|
|
211
|
+
|
|
212
|
+
const kvClient = createKvIndexClient({
|
|
213
|
+
// Optional: the Workers KV binding when running inside a Worker / Pages Function.
|
|
214
|
+
binding: env?.MY_INDEX_KV,
|
|
215
|
+
|
|
216
|
+
// Required: account + namespace identifiers for the REST fallback.
|
|
217
|
+
accountId: process.env.CF_ACCOUNT_ID!,
|
|
218
|
+
namespaceId: process.env.CF_NAMESPACE_ID!,
|
|
219
|
+
apiToken: process.env.CF_API_TOKEN!,
|
|
220
|
+
|
|
221
|
+
// Optional: custom fetch (defaults to globalThis.fetch).
|
|
222
|
+
fetch: globalThis.fetch,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// Example 1: expect at most one canonical match.
|
|
226
|
+
const [post] = await kvClient.queryIndex({
|
|
227
|
+
baseKey: 'post', // the client automatically prefixes keys with "idx:"
|
|
228
|
+
searchKey: 'slug',
|
|
229
|
+
uniqueKey: 'user-34329473094',
|
|
230
|
+
searchValue: 'blog:dasfadsfasd',
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Example 2: gather multiple raw index hits (no canonical pointer needed).
|
|
234
|
+
const relatedPosts = await kvClient.queryIndex({
|
|
235
|
+
baseKey: 'post',
|
|
236
|
+
searchKey: 'tag',
|
|
237
|
+
uniqueKey: 'user-34329473094',
|
|
238
|
+
searchValue: ['launch', 'beta'], // arrays fan out to multiple prefix searches
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// `post` is the canonical object (when exactly one match is found).
|
|
242
|
+
// `relatedPosts` is an array of parsed index meta objects (no manual JSON.parse needed).
|
|
243
|
+
|
|
244
|
+
// Fetch an exact key if you already know the canonical identifier.
|
|
245
|
+
const canonical = await kvClient.getKey('post:canonical:user-34329473094')
|
|
246
|
+
// -> returns an object (or null) and emits console.warn indicating whether the binding or API was used.
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Hydrating Collections from KV
|
|
250
|
+
|
|
251
|
+
Templates can declare `#array` blocks that describe KV-backed collections. Use `hydrateValues` to resolve those collections before rendering.
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import {
|
|
255
|
+
hydrateValues,
|
|
256
|
+
renderTemplate,
|
|
257
|
+
type HydrateValuesOptions,
|
|
258
|
+
} from '@edge/template-engine'
|
|
259
|
+
|
|
260
|
+
const options: HydrateValuesOptions = {
|
|
261
|
+
content: contentString,
|
|
262
|
+
values: initialValueOverrides,
|
|
263
|
+
meta: metaDefinitionOverride,
|
|
264
|
+
uniqueKey: 'workspace-1234', // the namespace segment used when indexing
|
|
265
|
+
clientOptions: {
|
|
266
|
+
// Optional binding (Workers runtime)
|
|
267
|
+
binding: env?.MY_INDEX_KV,
|
|
268
|
+
|
|
269
|
+
// REST fallback credentials (e.g. during SSR/build)
|
|
270
|
+
accountId: process.env.CF_ACCOUNT_ID!,
|
|
271
|
+
namespaceId: process.env.CF_NAMESPACE_ID!,
|
|
272
|
+
apiToken: process.env.CF_API_TOKEN!,
|
|
273
|
+
|
|
274
|
+
fetch: globalThis.fetch,
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const hydratedValues = await hydrateValues(options)
|
|
279
|
+
const html = renderTemplate(contentString, hydratedValues, metaDefinitionOverride)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Key details:
|
|
283
|
+
|
|
284
|
+
- `hydrateValues` reads any `collection`, `queryItems`, `queryOptions`, `order`, and `limit` settings from the template markup or meta overrides.
|
|
285
|
+
- Each query item fans out to `kvClient.queryIndex` calls, combining responses and deduplicating by `canonical`.
|
|
286
|
+
- Filters (`queryOptions`) and ordering rules execute in JS after the index lookups, and the final array is written back to the corresponding field in the returned `values`.
|
|
287
|
+
- If the collection cannot be fetched, the resolver falls back to any inline `value` you provided or leaves an empty array.
|
|
288
|
+
|
|
289
|
+
## UnoCSS SSR Helpers
|
|
290
|
+
|
|
291
|
+
When your rendered HTML relies on UnoCSS utilities, the package exposes helpers to generate the exact CSS during SSR or at build time.
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
import {
|
|
295
|
+
unoCssFromHtml,
|
|
296
|
+
normalizeTheme,
|
|
297
|
+
buildCssVarsBlock,
|
|
298
|
+
} from '@edge/template-engine'
|
|
299
|
+
|
|
300
|
+
const theme = {
|
|
301
|
+
extend: {
|
|
302
|
+
colors: {
|
|
303
|
+
brand: '#2563eb',
|
|
304
|
+
},
|
|
305
|
+
fontFamily: {
|
|
306
|
+
brand: ['Inter', 'sans-serif'],
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
variants: {
|
|
310
|
+
dark: {
|
|
311
|
+
extend: {
|
|
312
|
+
colors: {
|
|
313
|
+
brand: '#3b82f6',
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const { css, hash } = await unoCssFromHtml(renderedHtml, theme)
|
|
321
|
+
|
|
322
|
+
// Inject the CSS into your HTML shell, or cache it under the returned hash.
|
|
323
|
+
const cssVarsBlock = buildCssVarsBlock(theme) // optional: inline preflight variables yourself
|
|
324
|
+
const unoTheme = normalizeTheme(theme) // optional: feed into other Uno-powered tooling
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Details:
|
|
328
|
+
|
|
329
|
+
- `unoCssFromHtml(html, theme)` returns `{ css, hash }`, producing minified output that already includes preflights and the custom font-family rules bundled here.
|
|
330
|
+
- `normalizeTheme` strips the `extend` wrapper so your theme matches Uno’s expected shape if you need to share it elsewhere.
|
|
331
|
+
- `buildCssVarsBlock` mirrors the default preflight while letting you embed the CSS variables manually if desired.
|
|
332
|
+
- The generator is cached per theme hash, and transformer dependencies are loaded lazily; ensure your runtime supports dynamic `import()` (Node 18+, modern bundlers, Workers, etc.).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type KvIndexClientOptions } from './kvIndexClient';
|
|
2
|
+
import type { TemplateMeta, TemplateValues } from './types';
|
|
3
|
+
export interface HydrateValuesOptions {
|
|
4
|
+
content: string;
|
|
5
|
+
meta?: TemplateMeta;
|
|
6
|
+
values?: TemplateValues;
|
|
7
|
+
uniqueKey: string;
|
|
8
|
+
clientOptions: KvIndexClientOptions;
|
|
9
|
+
}
|
|
10
|
+
export declare const hydrateValues: (options: HydrateValuesOptions) => Promise<TemplateValues>;
|
|
11
|
+
//# sourceMappingURL=hydrateValues.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydrateValues.d.ts","sourceRoot":"","sources":["../src/hydrateValues.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,oBAAoB,EAE1B,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAKV,YAAY,EACZ,cAAc,EACf,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,oBAAoB,CAAA;CACpC;AAooBD,eAAO,MAAM,aAAa,GACxB,SAAS,oBAAoB,KAC5B,OAAO,CAAC,cAAc,CAqFxB,CAAA"}
|