@formfeed/devkit 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Philipp Staudt IT-Dienstleistungen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @formfeed/devkit
2
+
3
+ The local half of the [Formfeed](https://formfeed.dev) development kit: template folders, project
4
+ configuration (`formfeed.json`) and offline rendering with the same engine the API uses. The
5
+ [`formfeed` CLI](https://www.npmjs.com/package/formfeed) and
6
+ [`@formfeed/testing`](https://www.npmjs.com/package/@formfeed/testing) are built on it; use it
7
+ directly when you write your own tooling around a template repository.
8
+
9
+ ```bash
10
+ npm install @formfeed/devkit
11
+ ```
12
+
13
+ ```ts
14
+ import { defaultData, diagnose, projectAround, readTemplate, renderLocal } from '@formfeed/devkit';
15
+
16
+ // a folder with template.html, template.json, styles.css, data/*.json (see the project layout docs)
17
+ const project = projectAround('templates/invoice-de');
18
+ const tpl = readTemplate(project, 'invoice-de');
19
+ const { data } = defaultData(tpl); // data/default.json, or the first data set
20
+
21
+ console.log(diagnose(tpl, data)); // the editor's diagnostics: syntax errors, unknown variables
22
+ const { document } = await renderLocal(project, tpl, data, { mode: 'print' }); // complete HTML, no browser
23
+ ```
24
+
25
+ Errors are `DevkitError` instances with a `code`, which the CLI maps to its exit codes.
26
+
27
+ ## Documentation
28
+
29
+ - [Development kit overview](https://docs.formfeed.dev/devkit/overview)
30
+ - [Project layout](https://docs.formfeed.dev/devkit/project-layout)
31
+
32
+ ## Licence
33
+
34
+ MIT
package/index.cjs ADDED
@@ -0,0 +1,341 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // packages/devkit/src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DevkitError: () => DevkitError,
24
+ contentHash: () => contentHash,
25
+ defaultData: () => defaultData,
26
+ defaultProjectConfig: () => defaultProjectConfig,
27
+ diagnose: () => diagnose,
28
+ findProject: () => findProject,
29
+ listTemplateSlugs: () => listTemplateSlugs,
30
+ loadProject: () => loadProject,
31
+ partialResolver: () => partialResolver,
32
+ previewDocument: () => previewDocument,
33
+ projectAround: () => projectAround,
34
+ readState: () => readState,
35
+ readTemplate: () => readTemplate,
36
+ recordSync: () => recordSync,
37
+ renderContext: () => renderContext,
38
+ renderLocal: () => renderLocal,
39
+ templateDir: () => templateDir,
40
+ titleFromSlug: () => titleFromSlug,
41
+ versionPayload: () => versionPayload,
42
+ writeProjectConfig: () => writeProjectConfig,
43
+ writeState: () => writeState,
44
+ writeTemplate: () => writeTemplate
45
+ });
46
+ module.exports = __toCommonJS(index_exports);
47
+
48
+ // packages/devkit/src/lib/errors.ts
49
+ var DevkitError = class extends Error {
50
+ constructor(message, code = "usage") {
51
+ super(message);
52
+ this.code = code;
53
+ }
54
+ name = "DevkitError";
55
+ };
56
+
57
+ // packages/devkit/src/lib/project-config.ts
58
+ var import_node_fs = require("node:fs");
59
+ var import_node_path = require("node:path");
60
+ var defaultProjectConfig = {
61
+ templatesDir: "templates",
62
+ partialsDir: "partials",
63
+ engine: "jinja2",
64
+ ignore: ["**/drafts/**"]
65
+ };
66
+ function findProject(cwd = process.cwd()) {
67
+ let dir = (0, import_node_path.resolve)(cwd);
68
+ for (; ; ) {
69
+ const candidate = (0, import_node_path.join)(dir, "formfeed.json");
70
+ if ((0, import_node_fs.existsSync)(candidate)) return loadProject(candidate);
71
+ const parent = (0, import_node_path.dirname)(dir);
72
+ if (parent === dir) return null;
73
+ dir = parent;
74
+ }
75
+ }
76
+ function loadProject(configPath) {
77
+ let raw;
78
+ try {
79
+ raw = JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf8"));
80
+ } catch {
81
+ throw new DevkitError(`${configPath} is not valid JSON`);
82
+ }
83
+ const config = { ...defaultProjectConfig, ...raw };
84
+ const root = (0, import_node_path.dirname)(configPath);
85
+ return { root, configPath, config, templatesDir: (0, import_node_path.resolve)(root, config.templatesDir), partialsDir: (0, import_node_path.resolve)(root, config.partialsDir) };
86
+ }
87
+ function projectAround(templateFolder) {
88
+ const dir = (0, import_node_path.resolve)(templateFolder);
89
+ const found = findProject(dir);
90
+ if (found) return found;
91
+ const templatesDir = (0, import_node_path.dirname)(dir);
92
+ const root = (0, import_node_path.dirname)(templatesDir);
93
+ return {
94
+ root,
95
+ configPath: null,
96
+ config: { ...defaultProjectConfig, templatesDir, partialsDir: (0, import_node_path.join)(root, "partials") },
97
+ templatesDir,
98
+ partialsDir: (0, import_node_path.join)(root, "partials")
99
+ };
100
+ }
101
+ function writeProjectConfig(root, config) {
102
+ const path = (0, import_node_path.join)(root, "formfeed.json");
103
+ const full = { ...defaultProjectConfig, ...config };
104
+ (0, import_node_fs.writeFileSync)(path, JSON.stringify(full, null, 2) + "\n");
105
+ return path;
106
+ }
107
+
108
+ // packages/devkit/src/lib/project.ts
109
+ var import_node_crypto = require("node:crypto");
110
+ var import_node_fs2 = require("node:fs");
111
+ var import_node_path2 = require("node:path");
112
+ var readText = (path) => (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : null;
113
+ function readJson(path, what) {
114
+ const text = readText(path);
115
+ if (text === null) return null;
116
+ try {
117
+ return JSON.parse(text);
118
+ } catch (e) {
119
+ throw new DevkitError(`${what} is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`, "validation");
120
+ }
121
+ }
122
+ function listTemplateSlugs(project) {
123
+ if (!(0, import_node_fs2.existsSync)(project.templatesDir)) return [];
124
+ return (0, import_node_fs2.readdirSync)(project.templatesDir).filter((name) => (0, import_node_fs2.existsSync)((0, import_node_path2.join)(project.templatesDir, name, "template.html"))).sort();
125
+ }
126
+ function templateDir(project, slug) {
127
+ return (0, import_node_path2.join)(project.templatesDir, slug);
128
+ }
129
+ function readTemplate(project, slug) {
130
+ const dir = templateDir(project, slug);
131
+ const html = readText((0, import_node_path2.join)(dir, "template.html"));
132
+ if (html === null) throw new DevkitError(`No template "${slug}" in ${project.templatesDir} (template.html missing)`);
133
+ const metaFile = readJson((0, import_node_path2.join)(dir, "template.json"), "template.json") ?? {};
134
+ const meta = {
135
+ name: metaFile.name ?? titleFromSlug(slug),
136
+ kind: metaFile.kind ?? "pdf",
137
+ engine: metaFile.engine ?? project.config.engine,
138
+ description: metaFile.description ?? null,
139
+ tags: metaFile.tags ?? []
140
+ };
141
+ const settings = { ...readJson((0, import_node_path2.join)(dir, "settings.json"), "settings.json") ?? {} };
142
+ const header = readText((0, import_node_path2.join)(dir, "header.html"));
143
+ const footer = readText((0, import_node_path2.join)(dir, "footer.html"));
144
+ if (header !== null && header.trim()) settings.header = { ...settings.header ?? {}, html: header };
145
+ if (footer !== null && footer.trim()) settings.footer = { ...settings.footer ?? {}, html: footer };
146
+ const dataSets = {};
147
+ const dataDir = (0, import_node_path2.join)(dir, "data");
148
+ if ((0, import_node_fs2.existsSync)(dataDir)) {
149
+ for (const file of (0, import_node_fs2.readdirSync)(dataDir).filter((f) => f.endsWith(".json")).sort()) {
150
+ dataSets[(0, import_node_path2.basename)(file, ".json")] = readJson((0, import_node_path2.join)(dataDir, file), `data/${file}`);
151
+ }
152
+ }
153
+ return {
154
+ slug,
155
+ dir,
156
+ meta,
157
+ html,
158
+ css: readText((0, import_node_path2.join)(dir, "style.css")) ?? "",
159
+ head: readText((0, import_node_path2.join)(dir, "head.html")) ?? "",
160
+ settings,
161
+ dataSets,
162
+ dataSchema: readJson((0, import_node_path2.join)(dir, "schema.json"), "schema.json"),
163
+ i18n: readJson((0, import_node_path2.join)(dir, "i18n.json"), "i18n.json")
164
+ };
165
+ }
166
+ function defaultData(tpl, name) {
167
+ const names = Object.keys(tpl.dataSets);
168
+ const chosen = name ?? (names.includes("default") ? "default" : names[0]);
169
+ if (name && !(name in tpl.dataSets))
170
+ throw new DevkitError(`Data set "${name}" not found; available: ${names.join(", ") || "none"}`);
171
+ return chosen ? { name: chosen, data: tpl.dataSets[chosen] } : { name: "empty", data: {} };
172
+ }
173
+ function writeTemplate(project, slug, meta, version) {
174
+ const dir = templateDir(project, slug);
175
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.join)(dir, "data"), { recursive: true });
176
+ const settings = { ...version.settings ?? {} };
177
+ const header = settings.header?.html ?? "";
178
+ const footer = settings.footer?.html ?? "";
179
+ if (settings.header) settings.header = stripHtml(settings.header);
180
+ if (settings.footer) settings.footer = stripHtml(settings.footer);
181
+ (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "template.json"), JSON.stringify(meta, null, 2) + "\n");
182
+ (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "template.html"), version.html ?? "");
183
+ writeOrRemove((0, import_node_path2.join)(dir, "style.css"), version.css ?? "");
184
+ writeOrRemove((0, import_node_path2.join)(dir, "head.html"), version.head ?? "");
185
+ writeOrRemove((0, import_node_path2.join)(dir, "header.html"), header);
186
+ writeOrRemove((0, import_node_path2.join)(dir, "footer.html"), footer);
187
+ (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
188
+ (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "data", "default.json"), JSON.stringify(version.sample_data ?? {}, null, 2) + "\n");
189
+ for (const [name, data] of Object.entries(version.data_sets ?? {}))
190
+ if (name !== "default" && /^[\w.-]+$/.test(name))
191
+ (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "data", `${name}.json`), JSON.stringify(data ?? {}, null, 2) + "\n");
192
+ writeOrRemove((0, import_node_path2.join)(dir, "schema.json"), version.data_schema ? JSON.stringify(version.data_schema, null, 2) + "\n" : "");
193
+ writeOrRemove((0, import_node_path2.join)(dir, "i18n.json"), version.i18n ? JSON.stringify(version.i18n, null, 2) + "\n" : "");
194
+ return dir;
195
+ }
196
+ function stripHtml(part) {
197
+ const { html: _html, ...rest } = part;
198
+ void _html;
199
+ return Object.keys(rest).length ? rest : null;
200
+ }
201
+ function writeOrRemove(path, content) {
202
+ if (content) (0, import_node_fs2.writeFileSync)(path, content);
203
+ else if ((0, import_node_fs2.existsSync)(path)) (0, import_node_fs2.rmSync)(path);
204
+ }
205
+ function versionPayload(tpl) {
206
+ const sample = tpl.dataSets["default"] ?? Object.values(tpl.dataSets)[0] ?? {};
207
+ const named = Object.fromEntries(
208
+ Object.entries(tpl.dataSets).filter(([name]) => name !== "default")
209
+ );
210
+ return {
211
+ html: tpl.html,
212
+ css: tpl.css,
213
+ head: tpl.head,
214
+ settings: tpl.settings,
215
+ sample_data: sample ?? {},
216
+ data_sets: named,
217
+ data_schema: tpl.dataSchema,
218
+ i18n: tpl.i18n
219
+ };
220
+ }
221
+ function contentHash(tpl) {
222
+ const payload = versionPayload(tpl);
223
+ return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify([payload.html, payload.css, payload.head, payload.settings, payload.sample_data, payload.data_sets, payload.data_schema, payload.i18n])).digest("hex");
224
+ }
225
+ var statePath = (project) => (0, import_node_path2.join)(project.root, ".formfeed", "state.json");
226
+ function readState(project) {
227
+ return readJson(statePath(project), ".formfeed/state.json") ?? { templates: {} };
228
+ }
229
+ function writeState(project, state) {
230
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.join)(project.root, ".formfeed"), { recursive: true });
231
+ (0, import_node_fs2.writeFileSync)(statePath(project), JSON.stringify(state, null, 2) + "\n");
232
+ }
233
+ function recordSync(project, slug, version, tpl) {
234
+ const state = readState(project);
235
+ state.templates[slug] = {
236
+ checksum: version.checksum,
237
+ number: version.number,
238
+ status: version.status,
239
+ contentHash: contentHash(tpl),
240
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString()
241
+ };
242
+ writeState(project, state);
243
+ }
244
+ function titleFromSlug(slug) {
245
+ return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
246
+ }
247
+ function partialResolver(project) {
248
+ return (name) => {
249
+ for (const candidate of [name, `${name}.html`]) {
250
+ const path = (0, import_node_path2.join)(project.partialsDir, candidate);
251
+ if ((0, import_node_fs2.existsSync)(path) && (0, import_node_fs2.statSync)(path).isFile()) return (0, import_node_fs2.readFileSync)(path, "utf8");
252
+ }
253
+ return void 0;
254
+ };
255
+ }
256
+
257
+ // packages/devkit/src/lib/local-render.ts
258
+ var import_engine = require("@formfeed/engine");
259
+ function diagnose(tpl, sampleData) {
260
+ const engine = (0, import_engine.getEngine)(tpl.meta.engine);
261
+ const diagnostics = [];
262
+ try {
263
+ engine.compile(tpl.html, { name: tpl.slug });
264
+ } catch (e) {
265
+ if (e instanceof import_engine.EngineSyntaxError) {
266
+ diagnostics.push({
267
+ severity: "error",
268
+ code: "syntax-error",
269
+ message: e.message,
270
+ range: { start: { line: e.line ?? 1, column: e.column ?? 1 }, end: { line: e.line ?? 1, column: (e.column ?? 1) + 1 } }
271
+ });
272
+ } else throw e;
273
+ }
274
+ diagnostics.push(...engine.analyze(tpl.html, { sampleData }).diagnostics);
275
+ for (const [part, source] of [
276
+ ["header", tpl.settings.header?.html],
277
+ ["footer", tpl.settings.footer?.html]
278
+ ]) {
279
+ if (!source) continue;
280
+ for (const d of engine.analyze(source, { sampleData }).diagnostics) diagnostics.push({ ...d, message: `${part}: ${d.message}` });
281
+ }
282
+ return diagnostics;
283
+ }
284
+ function renderContext(project, tpl) {
285
+ const settings = (0, import_engine.mergeSettings)(tpl.settings);
286
+ return {
287
+ locale: settings.locale ?? "en",
288
+ timezone: settings.timezone ?? "UTC",
289
+ currency: settings.currency ?? "EUR",
290
+ partials: partialResolver(project),
291
+ helpers: (0, import_engine.defaultHelpers)(),
292
+ limits: import_engine.defaultLimits,
293
+ i18n: tpl.i18n ?? void 0
294
+ };
295
+ }
296
+ async function renderLocal(project, tpl, data, options) {
297
+ const ctx = renderContext(project, tpl);
298
+ if (options.locale) ctx.locale = options.locale;
299
+ return (0, import_engine.renderVersion)(
300
+ { engine: tpl.meta.engine, html: tpl.html, css: tpl.css, head: tpl.head, settings: (0, import_engine.mergeSettings)(tpl.settings), kind: tpl.meta.kind },
301
+ data,
302
+ ctx,
303
+ options.mode,
304
+ options.vendor ? { vendor: options.vendor } : {}
305
+ );
306
+ }
307
+ function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
308
+ const draft = {
309
+ document: rendered.document,
310
+ headerHtml: rendered.headerHtml,
311
+ footerHtml: rendered.footerHtml,
312
+ settings: rendered.settings,
313
+ kind: tpl.meta.kind
314
+ };
315
+ return mode === "paged" && tpl.meta.kind === "pdf" ? (0, import_engine.pagedDocument)(draft, { pagedScriptUrl }) : (0, import_engine.flowDocument)(draft);
316
+ }
317
+ // Annotate the CommonJS export names for ESM import in node:
318
+ 0 && (module.exports = {
319
+ DevkitError,
320
+ contentHash,
321
+ defaultData,
322
+ defaultProjectConfig,
323
+ diagnose,
324
+ findProject,
325
+ listTemplateSlugs,
326
+ loadProject,
327
+ partialResolver,
328
+ previewDocument,
329
+ projectAround,
330
+ readState,
331
+ readTemplate,
332
+ recordSync,
333
+ renderContext,
334
+ renderLocal,
335
+ templateDir,
336
+ titleFromSlug,
337
+ versionPayload,
338
+ writeProjectConfig,
339
+ writeState,
340
+ writeTemplate
341
+ });
package/index.js ADDED
@@ -0,0 +1,302 @@
1
+ // packages/devkit/src/lib/errors.ts
2
+ var DevkitError = class extends Error {
3
+ constructor(message, code = "usage") {
4
+ super(message);
5
+ this.code = code;
6
+ }
7
+ name = "DevkitError";
8
+ };
9
+
10
+ // packages/devkit/src/lib/project-config.ts
11
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { dirname, join, resolve } from "node:path";
13
+ var defaultProjectConfig = {
14
+ templatesDir: "templates",
15
+ partialsDir: "partials",
16
+ engine: "jinja2",
17
+ ignore: ["**/drafts/**"]
18
+ };
19
+ function findProject(cwd = process.cwd()) {
20
+ let dir = resolve(cwd);
21
+ for (; ; ) {
22
+ const candidate = join(dir, "formfeed.json");
23
+ if (existsSync(candidate)) return loadProject(candidate);
24
+ const parent = dirname(dir);
25
+ if (parent === dir) return null;
26
+ dir = parent;
27
+ }
28
+ }
29
+ function loadProject(configPath) {
30
+ let raw;
31
+ try {
32
+ raw = JSON.parse(readFileSync(configPath, "utf8"));
33
+ } catch {
34
+ throw new DevkitError(`${configPath} is not valid JSON`);
35
+ }
36
+ const config = { ...defaultProjectConfig, ...raw };
37
+ const root = dirname(configPath);
38
+ return { root, configPath, config, templatesDir: resolve(root, config.templatesDir), partialsDir: resolve(root, config.partialsDir) };
39
+ }
40
+ function projectAround(templateFolder) {
41
+ const dir = resolve(templateFolder);
42
+ const found = findProject(dir);
43
+ if (found) return found;
44
+ const templatesDir = dirname(dir);
45
+ const root = dirname(templatesDir);
46
+ return {
47
+ root,
48
+ configPath: null,
49
+ config: { ...defaultProjectConfig, templatesDir, partialsDir: join(root, "partials") },
50
+ templatesDir,
51
+ partialsDir: join(root, "partials")
52
+ };
53
+ }
54
+ function writeProjectConfig(root, config) {
55
+ const path = join(root, "formfeed.json");
56
+ const full = { ...defaultProjectConfig, ...config };
57
+ writeFileSync(path, JSON.stringify(full, null, 2) + "\n");
58
+ return path;
59
+ }
60
+
61
+ // packages/devkit/src/lib/project.ts
62
+ import { createHash } from "node:crypto";
63
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
64
+ import { basename, join as join2 } from "node:path";
65
+ var readText = (path) => existsSync2(path) ? readFileSync2(path, "utf8") : null;
66
+ function readJson(path, what) {
67
+ const text = readText(path);
68
+ if (text === null) return null;
69
+ try {
70
+ return JSON.parse(text);
71
+ } catch (e) {
72
+ throw new DevkitError(`${what} is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`, "validation");
73
+ }
74
+ }
75
+ function listTemplateSlugs(project) {
76
+ if (!existsSync2(project.templatesDir)) return [];
77
+ return readdirSync(project.templatesDir).filter((name) => existsSync2(join2(project.templatesDir, name, "template.html"))).sort();
78
+ }
79
+ function templateDir(project, slug) {
80
+ return join2(project.templatesDir, slug);
81
+ }
82
+ function readTemplate(project, slug) {
83
+ const dir = templateDir(project, slug);
84
+ const html = readText(join2(dir, "template.html"));
85
+ if (html === null) throw new DevkitError(`No template "${slug}" in ${project.templatesDir} (template.html missing)`);
86
+ const metaFile = readJson(join2(dir, "template.json"), "template.json") ?? {};
87
+ const meta = {
88
+ name: metaFile.name ?? titleFromSlug(slug),
89
+ kind: metaFile.kind ?? "pdf",
90
+ engine: metaFile.engine ?? project.config.engine,
91
+ description: metaFile.description ?? null,
92
+ tags: metaFile.tags ?? []
93
+ };
94
+ const settings = { ...readJson(join2(dir, "settings.json"), "settings.json") ?? {} };
95
+ const header = readText(join2(dir, "header.html"));
96
+ const footer = readText(join2(dir, "footer.html"));
97
+ if (header !== null && header.trim()) settings.header = { ...settings.header ?? {}, html: header };
98
+ if (footer !== null && footer.trim()) settings.footer = { ...settings.footer ?? {}, html: footer };
99
+ const dataSets = {};
100
+ const dataDir = join2(dir, "data");
101
+ if (existsSync2(dataDir)) {
102
+ for (const file of readdirSync(dataDir).filter((f) => f.endsWith(".json")).sort()) {
103
+ dataSets[basename(file, ".json")] = readJson(join2(dataDir, file), `data/${file}`);
104
+ }
105
+ }
106
+ return {
107
+ slug,
108
+ dir,
109
+ meta,
110
+ html,
111
+ css: readText(join2(dir, "style.css")) ?? "",
112
+ head: readText(join2(dir, "head.html")) ?? "",
113
+ settings,
114
+ dataSets,
115
+ dataSchema: readJson(join2(dir, "schema.json"), "schema.json"),
116
+ i18n: readJson(join2(dir, "i18n.json"), "i18n.json")
117
+ };
118
+ }
119
+ function defaultData(tpl, name) {
120
+ const names = Object.keys(tpl.dataSets);
121
+ const chosen = name ?? (names.includes("default") ? "default" : names[0]);
122
+ if (name && !(name in tpl.dataSets))
123
+ throw new DevkitError(`Data set "${name}" not found; available: ${names.join(", ") || "none"}`);
124
+ return chosen ? { name: chosen, data: tpl.dataSets[chosen] } : { name: "empty", data: {} };
125
+ }
126
+ function writeTemplate(project, slug, meta, version) {
127
+ const dir = templateDir(project, slug);
128
+ mkdirSync(join2(dir, "data"), { recursive: true });
129
+ const settings = { ...version.settings ?? {} };
130
+ const header = settings.header?.html ?? "";
131
+ const footer = settings.footer?.html ?? "";
132
+ if (settings.header) settings.header = stripHtml(settings.header);
133
+ if (settings.footer) settings.footer = stripHtml(settings.footer);
134
+ writeFileSync2(join2(dir, "template.json"), JSON.stringify(meta, null, 2) + "\n");
135
+ writeFileSync2(join2(dir, "template.html"), version.html ?? "");
136
+ writeOrRemove(join2(dir, "style.css"), version.css ?? "");
137
+ writeOrRemove(join2(dir, "head.html"), version.head ?? "");
138
+ writeOrRemove(join2(dir, "header.html"), header);
139
+ writeOrRemove(join2(dir, "footer.html"), footer);
140
+ writeFileSync2(join2(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
141
+ writeFileSync2(join2(dir, "data", "default.json"), JSON.stringify(version.sample_data ?? {}, null, 2) + "\n");
142
+ for (const [name, data] of Object.entries(version.data_sets ?? {}))
143
+ if (name !== "default" && /^[\w.-]+$/.test(name))
144
+ writeFileSync2(join2(dir, "data", `${name}.json`), JSON.stringify(data ?? {}, null, 2) + "\n");
145
+ writeOrRemove(join2(dir, "schema.json"), version.data_schema ? JSON.stringify(version.data_schema, null, 2) + "\n" : "");
146
+ writeOrRemove(join2(dir, "i18n.json"), version.i18n ? JSON.stringify(version.i18n, null, 2) + "\n" : "");
147
+ return dir;
148
+ }
149
+ function stripHtml(part) {
150
+ const { html: _html, ...rest } = part;
151
+ void _html;
152
+ return Object.keys(rest).length ? rest : null;
153
+ }
154
+ function writeOrRemove(path, content) {
155
+ if (content) writeFileSync2(path, content);
156
+ else if (existsSync2(path)) rmSync(path);
157
+ }
158
+ function versionPayload(tpl) {
159
+ const sample = tpl.dataSets["default"] ?? Object.values(tpl.dataSets)[0] ?? {};
160
+ const named = Object.fromEntries(
161
+ Object.entries(tpl.dataSets).filter(([name]) => name !== "default")
162
+ );
163
+ return {
164
+ html: tpl.html,
165
+ css: tpl.css,
166
+ head: tpl.head,
167
+ settings: tpl.settings,
168
+ sample_data: sample ?? {},
169
+ data_sets: named,
170
+ data_schema: tpl.dataSchema,
171
+ i18n: tpl.i18n
172
+ };
173
+ }
174
+ function contentHash(tpl) {
175
+ const payload = versionPayload(tpl);
176
+ return createHash("sha256").update(JSON.stringify([payload.html, payload.css, payload.head, payload.settings, payload.sample_data, payload.data_sets, payload.data_schema, payload.i18n])).digest("hex");
177
+ }
178
+ var statePath = (project) => join2(project.root, ".formfeed", "state.json");
179
+ function readState(project) {
180
+ return readJson(statePath(project), ".formfeed/state.json") ?? { templates: {} };
181
+ }
182
+ function writeState(project, state) {
183
+ mkdirSync(join2(project.root, ".formfeed"), { recursive: true });
184
+ writeFileSync2(statePath(project), JSON.stringify(state, null, 2) + "\n");
185
+ }
186
+ function recordSync(project, slug, version, tpl) {
187
+ const state = readState(project);
188
+ state.templates[slug] = {
189
+ checksum: version.checksum,
190
+ number: version.number,
191
+ status: version.status,
192
+ contentHash: contentHash(tpl),
193
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString()
194
+ };
195
+ writeState(project, state);
196
+ }
197
+ function titleFromSlug(slug) {
198
+ return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
199
+ }
200
+ function partialResolver(project) {
201
+ return (name) => {
202
+ for (const candidate of [name, `${name}.html`]) {
203
+ const path = join2(project.partialsDir, candidate);
204
+ if (existsSync2(path) && statSync(path).isFile()) return readFileSync2(path, "utf8");
205
+ }
206
+ return void 0;
207
+ };
208
+ }
209
+
210
+ // packages/devkit/src/lib/local-render.ts
211
+ import {
212
+ EngineSyntaxError,
213
+ defaultHelpers,
214
+ defaultLimits,
215
+ flowDocument,
216
+ getEngine,
217
+ mergeSettings,
218
+ pagedDocument,
219
+ renderVersion
220
+ } from "@formfeed/engine";
221
+ function diagnose(tpl, sampleData) {
222
+ const engine = getEngine(tpl.meta.engine);
223
+ const diagnostics = [];
224
+ try {
225
+ engine.compile(tpl.html, { name: tpl.slug });
226
+ } catch (e) {
227
+ if (e instanceof EngineSyntaxError) {
228
+ diagnostics.push({
229
+ severity: "error",
230
+ code: "syntax-error",
231
+ message: e.message,
232
+ range: { start: { line: e.line ?? 1, column: e.column ?? 1 }, end: { line: e.line ?? 1, column: (e.column ?? 1) + 1 } }
233
+ });
234
+ } else throw e;
235
+ }
236
+ diagnostics.push(...engine.analyze(tpl.html, { sampleData }).diagnostics);
237
+ for (const [part, source] of [
238
+ ["header", tpl.settings.header?.html],
239
+ ["footer", tpl.settings.footer?.html]
240
+ ]) {
241
+ if (!source) continue;
242
+ for (const d of engine.analyze(source, { sampleData }).diagnostics) diagnostics.push({ ...d, message: `${part}: ${d.message}` });
243
+ }
244
+ return diagnostics;
245
+ }
246
+ function renderContext(project, tpl) {
247
+ const settings = mergeSettings(tpl.settings);
248
+ return {
249
+ locale: settings.locale ?? "en",
250
+ timezone: settings.timezone ?? "UTC",
251
+ currency: settings.currency ?? "EUR",
252
+ partials: partialResolver(project),
253
+ helpers: defaultHelpers(),
254
+ limits: defaultLimits,
255
+ i18n: tpl.i18n ?? void 0
256
+ };
257
+ }
258
+ async function renderLocal(project, tpl, data, options) {
259
+ const ctx = renderContext(project, tpl);
260
+ if (options.locale) ctx.locale = options.locale;
261
+ return renderVersion(
262
+ { engine: tpl.meta.engine, html: tpl.html, css: tpl.css, head: tpl.head, settings: mergeSettings(tpl.settings), kind: tpl.meta.kind },
263
+ data,
264
+ ctx,
265
+ options.mode,
266
+ options.vendor ? { vendor: options.vendor } : {}
267
+ );
268
+ }
269
+ function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
270
+ const draft = {
271
+ document: rendered.document,
272
+ headerHtml: rendered.headerHtml,
273
+ footerHtml: rendered.footerHtml,
274
+ settings: rendered.settings,
275
+ kind: tpl.meta.kind
276
+ };
277
+ return mode === "paged" && tpl.meta.kind === "pdf" ? pagedDocument(draft, { pagedScriptUrl }) : flowDocument(draft);
278
+ }
279
+ export {
280
+ DevkitError,
281
+ contentHash,
282
+ defaultData,
283
+ defaultProjectConfig,
284
+ diagnose,
285
+ findProject,
286
+ listTemplateSlugs,
287
+ loadProject,
288
+ partialResolver,
289
+ previewDocument,
290
+ projectAround,
291
+ readState,
292
+ readTemplate,
293
+ recordSync,
294
+ renderContext,
295
+ renderLocal,
296
+ templateDir,
297
+ titleFromSlug,
298
+ versionPayload,
299
+ writeProjectConfig,
300
+ writeState,
301
+ writeTemplate
302
+ };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@formfeed/devkit",
3
+ "version": "0.1.0",
4
+ "description": "Formfeed template folders, project config and local rendering, shared by the CLI and @formfeed/testing.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./index.cjs",
8
+ "module": "./index.js",
9
+ "types": "./types/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./types/index.d.ts",
14
+ "default": "./index.js"
15
+ },
16
+ "require": {
17
+ "types": "./types/index.d.cts",
18
+ "default": "./index.cjs"
19
+ }
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "*.js",
25
+ "*.cjs",
26
+ "types",
27
+ "README.md",
28
+ "LICENSE",
29
+ "THIRD_PARTY_LICENSES.md"
30
+ ],
31
+ "sideEffects": false,
32
+ "dependencies": {
33
+ "@formfeed/engine": "0.1.0",
34
+ "@formfeed/sdk": "0.1.0"
35
+ },
36
+ "keywords": [
37
+ "formfeed",
38
+ "templates",
39
+ "pdf",
40
+ "devkit"
41
+ ],
42
+ "homepage": "https://docs.formfeed.dev/devkit/overview",
43
+ "author": {
44
+ "name": "Formfeed",
45
+ "email": "support@formfeed.dev",
46
+ "url": "https://formfeed.dev"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/formfeed-dev/formfeed.git",
51
+ "directory": "packages/devkit"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/formfeed-dev/formfeed/issues",
55
+ "email": "support@formfeed.dev"
56
+ },
57
+ "engines": {
58
+ "node": ">=22"
59
+ }
60
+ }
@@ -0,0 +1,7 @@
1
+ export { DevkitError } from './lib/errors.cjs';
2
+ export { defaultProjectConfig, findProject, loadProject, projectAround, writeProjectConfig } from './lib/project-config.cjs';
3
+ export type { Project, ProjectConfig } from './lib/project-config.cjs';
4
+ export { contentHash, defaultData, listTemplateSlugs, partialResolver, readState, readTemplate, recordSync, templateDir, titleFromSlug, versionPayload, writeState, writeTemplate, } from './lib/project.cjs';
5
+ export type { LocalTemplate, ProjectState, TemplateMeta, TemplateState } from './lib/project.cjs';
6
+ export { diagnose, previewDocument, renderContext, renderLocal } from './lib/local-render.cjs';
7
+ export type { LocalRenderOptions } from './lib/local-render.cjs';
@@ -0,0 +1,7 @@
1
+ export { DevkitError } from './lib/errors.js';
2
+ export { defaultProjectConfig, findProject, loadProject, projectAround, writeProjectConfig } from './lib/project-config.js';
3
+ export type { Project, ProjectConfig } from './lib/project-config.js';
4
+ export { contentHash, defaultData, listTemplateSlugs, partialResolver, readState, readTemplate, recordSync, templateDir, titleFromSlug, versionPayload, writeState, writeTemplate, } from './lib/project.js';
5
+ export type { LocalTemplate, ProjectState, TemplateMeta, TemplateState } from './lib/project.js';
6
+ export { diagnose, previewDocument, renderContext, renderLocal } from './lib/local-render.js';
7
+ export type { LocalRenderOptions } from './lib/local-render.js';
@@ -0,0 +1,6 @@
1
+ /** Errors of the development kit; the CLI maps `code` to its exit codes. */
2
+ export declare class DevkitError extends Error {
3
+ readonly code: 'usage' | 'validation';
4
+ readonly name = "DevkitError";
5
+ constructor(message: string, code?: 'usage' | 'validation');
6
+ }
@@ -0,0 +1,6 @@
1
+ /** Errors of the development kit; the CLI maps `code` to its exit codes. */
2
+ export declare class DevkitError extends Error {
3
+ readonly code: 'usage' | 'validation';
4
+ readonly name = "DevkitError";
5
+ constructor(message: string, code?: 'usage' | 'validation');
6
+ }
@@ -0,0 +1,15 @@
1
+ import { type AssembleVendor, type Diagnostic, type RenderContext, type RenderedDocument } from '@formfeed/engine';
2
+ import type { Project } from './project-config.cjs';
3
+ import { type LocalTemplate } from './project.cjs';
4
+ /** The same diagnostics the editor shows: analysis of the body against the sample data plus compile errors. */
5
+ export declare function diagnose(tpl: LocalTemplate, sampleData: unknown): Diagnostic[];
6
+ export declare function renderContext(project: Project, tpl: LocalTemplate): RenderContext;
7
+ export interface LocalRenderOptions {
8
+ mode: 'preview' | 'print';
9
+ locale?: string;
10
+ vendor?: AssembleVendor;
11
+ }
12
+ /** Assembles the complete document with the shared engine; no browser involved. */
13
+ export declare function renderLocal(project: Project, tpl: LocalTemplate, data: unknown, options: LocalRenderOptions): Promise<RenderedDocument>;
14
+ /** Wraps a preview render for the screen the way the editor does. */
15
+ export declare function previewDocument(tpl: LocalTemplate, rendered: RenderedDocument, mode: 'flow' | 'paged', pagedScriptUrl: string): string;
@@ -0,0 +1,15 @@
1
+ import { type AssembleVendor, type Diagnostic, type RenderContext, type RenderedDocument } from '@formfeed/engine';
2
+ import type { Project } from './project-config.js';
3
+ import { type LocalTemplate } from './project.js';
4
+ /** The same diagnostics the editor shows: analysis of the body against the sample data plus compile errors. */
5
+ export declare function diagnose(tpl: LocalTemplate, sampleData: unknown): Diagnostic[];
6
+ export declare function renderContext(project: Project, tpl: LocalTemplate): RenderContext;
7
+ export interface LocalRenderOptions {
8
+ mode: 'preview' | 'print';
9
+ locale?: string;
10
+ vendor?: AssembleVendor;
11
+ }
12
+ /** Assembles the complete document with the shared engine; no browser involved. */
13
+ export declare function renderLocal(project: Project, tpl: LocalTemplate, data: unknown, options: LocalRenderOptions): Promise<RenderedDocument>;
14
+ /** Wraps a preview render for the screen the way the editor does. */
15
+ export declare function previewDocument(tpl: LocalTemplate, rendered: RenderedDocument, mode: 'flow' | 'paged', pagedScriptUrl: string): string;
@@ -0,0 +1,28 @@
1
+ import type { EngineId } from '@formfeed/engine';
2
+ /** `formfeed.json` of a project (spec 15 §2). */
3
+ export interface ProjectConfig {
4
+ workspace?: string;
5
+ region?: 'eu' | 'us';
6
+ templatesDir: string;
7
+ partialsDir: string;
8
+ engine: EngineId;
9
+ ignore: string[];
10
+ }
11
+ export interface Project {
12
+ root: string;
13
+ /** Null for an ad-hoc project built around a template folder without formfeed.json. */
14
+ configPath: string | null;
15
+ config: ProjectConfig;
16
+ templatesDir: string;
17
+ partialsDir: string;
18
+ }
19
+ export declare const defaultProjectConfig: ProjectConfig;
20
+ /** Walks up from `cwd` to the nearest `formfeed.json`. */
21
+ export declare function findProject(cwd?: string): Project | null;
22
+ export declare function loadProject(configPath: string): Project;
23
+ /**
24
+ * A project for one template folder when no formfeed.json is around (test helpers): the folder's
25
+ * parent is the templates dir, `partials` next to it holds the partials.
26
+ */
27
+ export declare function projectAround(templateFolder: string): Project;
28
+ export declare function writeProjectConfig(root: string, config: Partial<ProjectConfig>): string;
@@ -0,0 +1,28 @@
1
+ import type { EngineId } from '@formfeed/engine';
2
+ /** `formfeed.json` of a project (spec 15 §2). */
3
+ export interface ProjectConfig {
4
+ workspace?: string;
5
+ region?: 'eu' | 'us';
6
+ templatesDir: string;
7
+ partialsDir: string;
8
+ engine: EngineId;
9
+ ignore: string[];
10
+ }
11
+ export interface Project {
12
+ root: string;
13
+ /** Null for an ad-hoc project built around a template folder without formfeed.json. */
14
+ configPath: string | null;
15
+ config: ProjectConfig;
16
+ templatesDir: string;
17
+ partialsDir: string;
18
+ }
19
+ export declare const defaultProjectConfig: ProjectConfig;
20
+ /** Walks up from `cwd` to the nearest `formfeed.json`. */
21
+ export declare function findProject(cwd?: string): Project | null;
22
+ export declare function loadProject(configPath: string): Project;
23
+ /**
24
+ * A project for one template folder when no formfeed.json is around (test helpers): the folder's
25
+ * parent is the templates dir, `partials` next to it holds the partials.
26
+ */
27
+ export declare function projectAround(templateFolder: string): Project;
28
+ export declare function writeProjectConfig(root: string, config: Partial<ProjectConfig>): string;
@@ -0,0 +1,72 @@
1
+ import type { EngineId, TemplateKind, TemplateSettings } from '@formfeed/engine';
2
+ import type { TemplateVersion } from '@formfeed/sdk';
3
+ import type { Project } from './project-config.cjs';
4
+ /**
5
+ * The template folder of spec 15 §2. `template.json` (name, kind, engine, description, tags) is
6
+ * the one addition to the spec's layout: `push` needs it to create a template that does not exist
7
+ * remotely, `pull` writes it. Header and footer live in their own files and fold back into
8
+ * `settings.header.html` / `settings.footer.html`, as the editor's tabs do.
9
+ */
10
+ export interface TemplateMeta {
11
+ name: string;
12
+ kind: TemplateKind;
13
+ engine: EngineId;
14
+ description?: string | null;
15
+ tags?: string[];
16
+ }
17
+ export interface LocalTemplate {
18
+ slug: string;
19
+ dir: string;
20
+ meta: TemplateMeta;
21
+ html: string;
22
+ css: string;
23
+ head: string;
24
+ /** Settings with header/footer html folded in. */
25
+ settings: TemplateSettings;
26
+ /** `data/<name>.json`, `default` first when present. */
27
+ dataSets: Record<string, unknown>;
28
+ dataSchema: Record<string, unknown> | null;
29
+ i18n: Record<string, Record<string, string>> | null;
30
+ }
31
+ export interface TemplateState {
32
+ /** Checksum of the remote version the folder was pulled from or pushed as. */
33
+ checksum: string;
34
+ number: number;
35
+ status: string;
36
+ /** Hash of the local files at that moment; a different hash means local edits. */
37
+ contentHash: string;
38
+ syncedAt: string;
39
+ }
40
+ export interface ProjectState {
41
+ templates: Record<string, TemplateState>;
42
+ }
43
+ export declare function listTemplateSlugs(project: Project): string[];
44
+ export declare function templateDir(project: Project, slug: string): string;
45
+ export declare function readTemplate(project: Project, slug: string): LocalTemplate;
46
+ /** The sample data set the preview and validation use: `default`, else the first file, else `{}`. */
47
+ export declare function defaultData(tpl: LocalTemplate, name?: string): {
48
+ name: string;
49
+ data: unknown;
50
+ };
51
+ export declare function writeTemplate(project: Project, slug: string, meta: TemplateMeta, version: Pick<TemplateVersion, 'html' | 'css' | 'head' | 'settings' | 'sample_data' | 'data_schema' | 'i18n'> & {
52
+ data_sets?: Record<string, unknown> | null;
53
+ }): string;
54
+ /** Files of a push request: what the API's TemplateVersionCreate takes. */
55
+ export declare function versionPayload(tpl: LocalTemplate): {
56
+ html: string;
57
+ css: string;
58
+ head: string;
59
+ settings: Record<string, unknown>;
60
+ sample_data: Record<string, unknown>;
61
+ data_sets: Record<string, unknown>;
62
+ data_schema: Record<string, unknown> | null;
63
+ i18n: Record<string, Record<string, string>> | null;
64
+ };
65
+ /** Stable hash of the local files; compared with the state to detect local edits. */
66
+ export declare function contentHash(tpl: LocalTemplate): string;
67
+ export declare function readState(project: Project): ProjectState;
68
+ export declare function writeState(project: Project, state: ProjectState): void;
69
+ export declare function recordSync(project: Project, slug: string, version: Pick<TemplateVersion, 'checksum' | 'number' | 'status'>, tpl: LocalTemplate): void;
70
+ export declare function titleFromSlug(slug: string): string;
71
+ /** Resolves `{% include %}` / partials from the project's partials folder (`name` or `name.html`). */
72
+ export declare function partialResolver(project: Project): (name: string) => string | undefined;
@@ -0,0 +1,72 @@
1
+ import type { EngineId, TemplateKind, TemplateSettings } from '@formfeed/engine';
2
+ import type { TemplateVersion } from '@formfeed/sdk';
3
+ import type { Project } from './project-config.js';
4
+ /**
5
+ * The template folder of spec 15 §2. `template.json` (name, kind, engine, description, tags) is
6
+ * the one addition to the spec's layout: `push` needs it to create a template that does not exist
7
+ * remotely, `pull` writes it. Header and footer live in their own files and fold back into
8
+ * `settings.header.html` / `settings.footer.html`, as the editor's tabs do.
9
+ */
10
+ export interface TemplateMeta {
11
+ name: string;
12
+ kind: TemplateKind;
13
+ engine: EngineId;
14
+ description?: string | null;
15
+ tags?: string[];
16
+ }
17
+ export interface LocalTemplate {
18
+ slug: string;
19
+ dir: string;
20
+ meta: TemplateMeta;
21
+ html: string;
22
+ css: string;
23
+ head: string;
24
+ /** Settings with header/footer html folded in. */
25
+ settings: TemplateSettings;
26
+ /** `data/<name>.json`, `default` first when present. */
27
+ dataSets: Record<string, unknown>;
28
+ dataSchema: Record<string, unknown> | null;
29
+ i18n: Record<string, Record<string, string>> | null;
30
+ }
31
+ export interface TemplateState {
32
+ /** Checksum of the remote version the folder was pulled from or pushed as. */
33
+ checksum: string;
34
+ number: number;
35
+ status: string;
36
+ /** Hash of the local files at that moment; a different hash means local edits. */
37
+ contentHash: string;
38
+ syncedAt: string;
39
+ }
40
+ export interface ProjectState {
41
+ templates: Record<string, TemplateState>;
42
+ }
43
+ export declare function listTemplateSlugs(project: Project): string[];
44
+ export declare function templateDir(project: Project, slug: string): string;
45
+ export declare function readTemplate(project: Project, slug: string): LocalTemplate;
46
+ /** The sample data set the preview and validation use: `default`, else the first file, else `{}`. */
47
+ export declare function defaultData(tpl: LocalTemplate, name?: string): {
48
+ name: string;
49
+ data: unknown;
50
+ };
51
+ export declare function writeTemplate(project: Project, slug: string, meta: TemplateMeta, version: Pick<TemplateVersion, 'html' | 'css' | 'head' | 'settings' | 'sample_data' | 'data_schema' | 'i18n'> & {
52
+ data_sets?: Record<string, unknown> | null;
53
+ }): string;
54
+ /** Files of a push request: what the API's TemplateVersionCreate takes. */
55
+ export declare function versionPayload(tpl: LocalTemplate): {
56
+ html: string;
57
+ css: string;
58
+ head: string;
59
+ settings: Record<string, unknown>;
60
+ sample_data: Record<string, unknown>;
61
+ data_sets: Record<string, unknown>;
62
+ data_schema: Record<string, unknown> | null;
63
+ i18n: Record<string, Record<string, string>> | null;
64
+ };
65
+ /** Stable hash of the local files; compared with the state to detect local edits. */
66
+ export declare function contentHash(tpl: LocalTemplate): string;
67
+ export declare function readState(project: Project): ProjectState;
68
+ export declare function writeState(project: Project, state: ProjectState): void;
69
+ export declare function recordSync(project: Project, slug: string, version: Pick<TemplateVersion, 'checksum' | 'number' | 'status'>, tpl: LocalTemplate): void;
70
+ export declare function titleFromSlug(slug: string): string;
71
+ /** Resolves `{% include %}` / partials from the project's partials folder (`name` or `name.html`). */
72
+ export declare function partialResolver(project: Project): (name: string) => string | undefined;