@formfeed/devkit 0.1.0 → 0.3.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/index.cjs CHANGED
@@ -21,24 +21,50 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  DevkitError: () => DevkitError,
24
+ brandFromJson: () => brandFromJson,
25
+ brandPath: () => brandPath,
26
+ collectPartials: () => collectPartials,
24
27
  contentHash: () => contentHash,
28
+ contentTypeFor: () => contentTypeFor,
25
29
  defaultData: () => defaultData,
26
30
  defaultProjectConfig: () => defaultProjectConfig,
31
+ defaultTypesFile: () => defaultTypesFile,
27
32
  diagnose: () => diagnose,
28
33
  findProject: () => findProject,
34
+ generateTypes: () => generateTypes,
35
+ listLocalFiles: () => listLocalFiles,
29
36
  listTemplateSlugs: () => listTemplateSlugs,
30
37
  loadProject: () => loadProject,
38
+ localFilePath: () => localFilePath,
39
+ localTypeSource: () => localTypeSource,
40
+ officeSnapshot: () => officeSnapshot,
41
+ officeVersionPayload: () => officeVersionPayload,
42
+ parseRedactPath: () => parseRedactPath,
43
+ partialContentHash: () => partialContentHash,
31
44
  partialResolver: () => partialResolver,
45
+ prettyXml: () => prettyXml,
32
46
  previewDocument: () => previewDocument,
33
47
  projectAround: () => projectAround,
48
+ readBrand: () => readBrand,
34
49
  readState: () => readState,
35
50
  readTemplate: () => readTemplate,
51
+ recordSharedPartial: () => recordSharedPartial,
36
52
  recordSync: () => recordSync,
53
+ redactData: () => redactData,
54
+ redactString: () => redactString,
37
55
  renderContext: () => renderContext,
38
56
  renderLocal: () => renderLocal,
57
+ renderOfficeLocal: () => renderOfficeLocal,
58
+ sha256File: () => sha256File,
59
+ sharedPartialNames: () => sharedPartialNames,
60
+ sharedPartialPath: () => sharedPartialPath,
39
61
  templateDir: () => templateDir,
62
+ templateFileName: () => templateFileName,
40
63
  titleFromSlug: () => titleFromSlug,
41
64
  versionPayload: () => versionPayload,
65
+ writeBrand: () => writeBrand,
66
+ writeLocalFile: () => writeLocalFile,
67
+ writePartials: () => writePartials,
42
68
  writeProjectConfig: () => writeProjectConfig,
43
69
  writeState: () => writeState,
44
70
  writeTemplate: () => writeTemplate
@@ -60,6 +86,7 @@ var import_node_path = require("node:path");
60
86
  var defaultProjectConfig = {
61
87
  templatesDir: "templates",
62
88
  partialsDir: "partials",
89
+ filesDir: "files",
63
90
  engine: "jinja2",
64
91
  ignore: ["**/drafts/**"]
65
92
  };
@@ -82,7 +109,14 @@ function loadProject(configPath) {
82
109
  }
83
110
  const config = { ...defaultProjectConfig, ...raw };
84
111
  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) };
112
+ return {
113
+ root,
114
+ configPath,
115
+ config,
116
+ templatesDir: (0, import_node_path.resolve)(root, config.templatesDir),
117
+ partialsDir: (0, import_node_path.resolve)(root, config.partialsDir),
118
+ filesDir: (0, import_node_path.resolve)(root, config.filesDir)
119
+ };
86
120
  }
87
121
  function projectAround(templateFolder) {
88
122
  const dir = (0, import_node_path.resolve)(templateFolder);
@@ -93,9 +127,10 @@ function projectAround(templateFolder) {
93
127
  return {
94
128
  root,
95
129
  configPath: null,
96
- config: { ...defaultProjectConfig, templatesDir, partialsDir: (0, import_node_path.join)(root, "partials") },
130
+ config: { ...defaultProjectConfig, templatesDir, partialsDir: (0, import_node_path.join)(root, "partials"), filesDir: (0, import_node_path.join)(root, "files") },
97
131
  templatesDir,
98
- partialsDir: (0, import_node_path.join)(root, "partials")
132
+ partialsDir: (0, import_node_path.join)(root, "partials"),
133
+ filesDir: (0, import_node_path.join)(root, "files")
99
134
  };
100
135
  }
101
136
  function writeProjectConfig(root, config) {
@@ -109,58 +144,123 @@ function writeProjectConfig(root, config) {
109
144
  var import_node_crypto = require("node:crypto");
110
145
  var import_node_fs2 = require("node:fs");
111
146
  var import_node_path2 = require("node:path");
147
+ var import_engine2 = require("@formfeed/engine");
148
+
149
+ // packages/devkit/src/lib/partials.ts
150
+ var import_engine = require("@formfeed/engine");
151
+ function includeNames(engine, source) {
152
+ try {
153
+ return (0, import_engine.getEngine)(engine).analyze(source).includes.map((include) => include.name);
154
+ } catch {
155
+ return [];
156
+ }
157
+ }
158
+ function collectPartials(project, engine, sources) {
159
+ const resolve3 = partialResolver(project);
160
+ const sharedNames = sharedPartialNames(project, engine);
161
+ const partials = {};
162
+ const missing = /* @__PURE__ */ new Set();
163
+ const shared = /* @__PURE__ */ new Set();
164
+ const queue = sources.filter((source) => Boolean(source));
165
+ const seen = /* @__PURE__ */ new Set();
166
+ while (queue.length) {
167
+ for (const name of includeNames(engine, queue.shift())) {
168
+ if (seen.has(name)) continue;
169
+ seen.add(name);
170
+ const source = resolve3(name);
171
+ if (sharedNames.has((0, import_engine.normalisePartialName)(name))) {
172
+ shared.add((0, import_engine.normalisePartialName)(name));
173
+ if (source !== void 0) queue.push(source);
174
+ continue;
175
+ }
176
+ if (source === void 0) {
177
+ missing.add(name);
178
+ continue;
179
+ }
180
+ partials[name] = source;
181
+ queue.push(source);
182
+ }
183
+ }
184
+ return { partials, missing: [...missing].sort(), shared: [...shared].sort() };
185
+ }
186
+
187
+ // packages/devkit/src/lib/project.ts
112
188
  var readText = (path) => (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : null;
113
189
  function readJson(path, what) {
114
- const text = readText(path);
115
- if (text === null) return null;
190
+ const text2 = readText(path);
191
+ if (text2 === null) return null;
116
192
  try {
117
- return JSON.parse(text);
193
+ return JSON.parse(text2);
118
194
  } catch (e) {
119
195
  throw new DevkitError(`${what} is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`, "validation");
120
196
  }
121
197
  }
198
+ var templateFileName = (kind) => (0, import_engine2.isOfficeKind)(kind) ? `template.${kind}` : "template.html";
199
+ function sourceFilesIn(dir) {
200
+ return ["template.html", ...import_engine2.officeKinds.map((k) => `template.${k}`)].filter((name) => (0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, name)));
201
+ }
122
202
  function listTemplateSlugs(project) {
123
203
  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();
204
+ return (0, import_node_fs2.readdirSync)(project.templatesDir).filter((name) => sourceFilesIn((0, import_node_path2.join)(project.templatesDir, name)).length > 0).sort();
125
205
  }
126
206
  function templateDir(project, slug) {
127
207
  return (0, import_node_path2.join)(project.templatesDir, slug);
128
208
  }
209
+ var sha256Of = (bytes) => (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
210
+ function readOfficeFile(dir, name) {
211
+ const path = (0, import_node_path2.join)(dir, name);
212
+ const bytes = new Uint8Array((0, import_node_fs2.readFileSync)(path));
213
+ return { format: (0, import_node_path2.extname)(name).slice(1), path, bytes, sha256: sha256Of(bytes) };
214
+ }
129
215
  function readTemplate(project, slug) {
130
216
  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)`);
217
+ const sources = sourceFilesIn(dir);
218
+ if (sources.length === 0)
219
+ throw new DevkitError(`No template "${slug}" in ${project.templatesDir} (template.html, template.docx or template.pptx missing)`);
220
+ if (sources.length > 1)
221
+ throw new DevkitError(`${dir} holds ${sources.join(" and ")}; a template folder has one of them`, "validation");
222
+ const file = sources[0] === "template.html" ? null : readOfficeFile(dir, sources[0]);
223
+ const html = file ? "" : readText((0, import_node_path2.join)(dir, "template.html")) ?? "";
133
224
  const metaFile = readJson((0, import_node_path2.join)(dir, "template.json"), "template.json") ?? {};
225
+ if (file && metaFile.kind && metaFile.kind !== file.format)
226
+ throw new DevkitError(`${slug}: template.json says kind "${metaFile.kind}", but the folder holds ${(0, import_node_path2.basename)(file.path)}`, "validation");
227
+ if (!file && (0, import_engine2.isOfficeKind)(metaFile.kind))
228
+ throw new DevkitError(`${slug}: template.json says kind "${metaFile.kind}", but the folder holds template.html instead of template.${metaFile.kind}`, "validation");
134
229
  const meta = {
135
230
  name: metaFile.name ?? titleFromSlug(slug),
136
- kind: metaFile.kind ?? "pdf",
231
+ kind: file?.format ?? metaFile.kind ?? "pdf",
137
232
  engine: metaFile.engine ?? project.config.engine,
138
233
  description: metaFile.description ?? null,
139
234
  tags: metaFile.tags ?? []
140
235
  };
141
236
  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"));
237
+ const header = file ? null : readText((0, import_node_path2.join)(dir, "header.html"));
238
+ const footer = file ? null : readText((0, import_node_path2.join)(dir, "footer.html"));
144
239
  if (header !== null && header.trim()) settings.header = { ...settings.header ?? {}, html: header };
145
240
  if (footer !== null && footer.trim()) settings.footer = { ...settings.footer ?? {}, html: footer };
146
241
  const dataSets = {};
147
242
  const dataDir = (0, import_node_path2.join)(dir, "data");
148
243
  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}`);
244
+ for (const file2 of (0, import_node_fs2.readdirSync)(dataDir).filter((f) => f.endsWith(".json")).sort()) {
245
+ dataSets[(0, import_node_path2.basename)(file2, ".json")] = readJson((0, import_node_path2.join)(dataDir, file2), `data/${file2}`);
151
246
  }
152
247
  }
248
+ const { partials, missing, shared } = file ? { partials: {}, missing: [], shared: [] } : collectPartials(project, meta.engine, [html, settings.header?.html, settings.footer?.html]);
153
249
  return {
154
250
  slug,
155
251
  dir,
156
252
  meta,
253
+ file,
157
254
  html,
158
- css: readText((0, import_node_path2.join)(dir, "style.css")) ?? "",
159
- head: readText((0, import_node_path2.join)(dir, "head.html")) ?? "",
255
+ css: file ? "" : readText((0, import_node_path2.join)(dir, "style.css")) ?? "",
256
+ head: file ? "" : readText((0, import_node_path2.join)(dir, "head.html")) ?? "",
160
257
  settings,
161
258
  dataSets,
162
259
  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")
260
+ i18n: readJson((0, import_node_path2.join)(dir, "i18n.json"), "i18n.json"),
261
+ partials,
262
+ missingPartials: missing,
263
+ sharedPartials: shared
164
264
  };
165
265
  }
166
266
  function defaultData(tpl, name) {
@@ -170,18 +270,23 @@ function defaultData(tpl, name) {
170
270
  throw new DevkitError(`Data set "${name}" not found; available: ${names.join(", ") || "none"}`);
171
271
  return chosen ? { name: chosen, data: tpl.dataSets[chosen] } : { name: "empty", data: {} };
172
272
  }
173
- function writeTemplate(project, slug, meta, version) {
273
+ function writeTemplate(project, slug, meta, version, file) {
174
274
  const dir = templateDir(project, slug);
275
+ const office = (0, import_engine2.isOfficeKind)(meta.kind);
276
+ if (office && !file) throw new DevkitError(`${slug} is a ${meta.kind} template; its document is needed to write the folder`);
175
277
  (0, import_node_fs2.mkdirSync)((0, import_node_path2.join)(dir, "data"), { recursive: true });
176
278
  const settings = { ...version.settings ?? {} };
177
- const header = settings.header?.html ?? "";
178
- const footer = settings.footer?.html ?? "";
279
+ const header = office ? "" : settings.header?.html ?? "";
280
+ const footer = office ? "" : settings.footer?.html ?? "";
179
281
  if (settings.header) settings.header = stripHtml(settings.header);
180
282
  if (settings.footer) settings.footer = stripHtml(settings.footer);
181
283
  (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 ?? "");
284
+ for (const name of ["template.html", ...import_engine2.officeKinds.map((k) => `template.${k}`)])
285
+ if (name !== templateFileName(meta.kind) && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, name))) (0, import_node_fs2.rmSync)((0, import_node_path2.join)(dir, name));
286
+ if (office) (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, templateFileName(meta.kind)), file);
287
+ else (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "template.html"), version.html ?? "");
288
+ writeOrRemove((0, import_node_path2.join)(dir, "style.css"), office ? "" : version.css ?? "");
289
+ writeOrRemove((0, import_node_path2.join)(dir, "head.html"), office ? "" : version.head ?? "");
185
290
  writeOrRemove((0, import_node_path2.join)(dir, "header.html"), header);
186
291
  writeOrRemove((0, import_node_path2.join)(dir, "footer.html"), footer);
187
292
  (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
@@ -191,8 +296,22 @@ function writeTemplate(project, slug, meta, version) {
191
296
  (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "data", `${name}.json`), JSON.stringify(data ?? {}, null, 2) + "\n");
192
297
  writeOrRemove((0, import_node_path2.join)(dir, "schema.json"), version.data_schema ? JSON.stringify(version.data_schema, null, 2) + "\n" : "");
193
298
  writeOrRemove((0, import_node_path2.join)(dir, "i18n.json"), version.i18n ? JSON.stringify(version.i18n, null, 2) + "\n" : "");
299
+ writePartials(project, version.partials ?? null);
194
300
  return dir;
195
301
  }
302
+ var partialName = /^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/;
303
+ function writePartials(project, partials) {
304
+ const changed = [];
305
+ for (const [name, source] of Object.entries(partials ?? {})) {
306
+ if (!partialName.test(name)) continue;
307
+ const path = (0, import_node_path2.join)(project.partialsDir, name.includes(".") ? name : `${name}.html`);
308
+ if (readText(path) === source) continue;
309
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
310
+ (0, import_node_fs2.writeFileSync)(path, source);
311
+ changed.push(path);
312
+ }
313
+ return changed;
314
+ }
196
315
  function stripHtml(part) {
197
316
  const { html: _html, ...rest } = part;
198
317
  void _html;
@@ -202,15 +321,12 @@ function writeOrRemove(path, content) {
202
321
  if (content) (0, import_node_fs2.writeFileSync)(path, content);
203
322
  else if ((0, import_node_fs2.existsSync)(path)) (0, import_node_fs2.rmSync)(path);
204
323
  }
205
- function versionPayload(tpl) {
324
+ function commonPayload(tpl) {
206
325
  const sample = tpl.dataSets["default"] ?? Object.values(tpl.dataSets)[0] ?? {};
207
326
  const named = Object.fromEntries(
208
327
  Object.entries(tpl.dataSets).filter(([name]) => name !== "default")
209
328
  );
210
329
  return {
211
- html: tpl.html,
212
- css: tpl.css,
213
- head: tpl.head,
214
330
  settings: tpl.settings,
215
331
  sample_data: sample ?? {},
216
332
  data_sets: named,
@@ -218,9 +334,29 @@ function versionPayload(tpl) {
218
334
  i18n: tpl.i18n
219
335
  };
220
336
  }
337
+ function officeVersionPayload(tpl) {
338
+ return commonPayload(tpl);
339
+ }
340
+ function versionPayload(tpl) {
341
+ return {
342
+ html: tpl.html,
343
+ css: tpl.css,
344
+ head: tpl.head,
345
+ ...commonPayload(tpl),
346
+ // only when the template includes one, so a project without partials sends what it always sent
347
+ ...Object.keys(tpl.partials).length ? { partials: tpl.partials } : {}
348
+ };
349
+ }
221
350
  function contentHash(tpl) {
351
+ if (tpl.file) {
352
+ const payload2 = officeVersionPayload(tpl);
353
+ const parts2 = [tpl.file.format, tpl.file.sha256, payload2.settings, payload2.sample_data, payload2.data_sets, payload2.data_schema, payload2.i18n];
354
+ return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(parts2)).digest("hex");
355
+ }
222
356
  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");
357
+ const parts = [payload.html, payload.css, payload.head, payload.settings, payload.sample_data, payload.data_sets, payload.data_schema, payload.i18n];
358
+ if (payload.partials) parts.push(payload.partials);
359
+ return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(parts)).digest("hex");
224
360
  }
225
361
  var statePath = (project) => (0, import_node_path2.join)(project.root, ".formfeed", "state.json");
226
362
  function readState(project) {
@@ -237,6 +373,7 @@ function recordSync(project, slug, version, tpl) {
237
373
  number: version.number,
238
374
  status: version.status,
239
375
  contentHash: contentHash(tpl),
376
+ ...tpl.file ? { fileSha256: tpl.file.sha256 } : {},
240
377
  syncedAt: (/* @__PURE__ */ new Date()).toISOString()
241
378
  };
242
379
  writeState(project, state);
@@ -245,24 +382,85 @@ function titleFromSlug(slug) {
245
382
  return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
246
383
  }
247
384
  function partialResolver(project) {
385
+ let shared = null;
248
386
  return (name) => {
249
387
  for (const candidate of [name, `${name}.html`]) {
250
388
  const path = (0, import_node_path2.join)(project.partialsDir, candidate);
251
389
  if ((0, import_node_fs2.existsSync)(path) && (0, import_node_fs2.statSync)(path).isFile()) return (0, import_node_fs2.readFileSync)(path, "utf8");
252
390
  }
253
- return void 0;
391
+ shared ??= new Set(Object.keys(readState(project).sharedPartials ?? {}));
392
+ const normalised = (0, import_engine2.normalisePartialName)(name);
393
+ if (!shared.has(normalised)) return void 0;
394
+ return readText(sharedPartialPath(project, normalised)) ?? void 0;
395
+ };
396
+ }
397
+ function sharedPartialPath(project, name) {
398
+ return (0, import_node_path2.join)(project.partialsDir, `${name}.html`);
399
+ }
400
+ function sharedPartialNames(project, engine) {
401
+ return new Set(
402
+ Object.entries(readState(project).sharedPartials ?? {}).filter(([, partial]) => partial.engine === engine).map(([name]) => (0, import_engine2.normalisePartialName)(name))
403
+ );
404
+ }
405
+ var partialContentHash = (source) => (0, import_node_crypto.createHash)("sha256").update(source).digest("hex");
406
+ function recordSharedPartial(project, name, remote, source) {
407
+ const state = readState(project);
408
+ state.sharedPartials = {
409
+ ...state.sharedPartials ?? {},
410
+ [name]: { version: remote.version, engine: remote.engine, contentHash: partialContentHash(source), syncedAt: (/* @__PURE__ */ new Date()).toISOString() }
411
+ };
412
+ writeState(project, state);
413
+ }
414
+
415
+ // packages/devkit/src/lib/brand.ts
416
+ var import_node_fs3 = require("node:fs");
417
+ var import_node_path3 = require("node:path");
418
+ var import_engine3 = require("@formfeed/engine");
419
+ var brandPath = (project) => (0, import_node_path3.join)(project.root, ".formfeed", "brand.json");
420
+ var text = (value) => typeof value === "string" ? value : null;
421
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
422
+ function brandFromJson(json) {
423
+ const body = record(json);
424
+ const empty = (0, import_engine3.emptyBrand)();
425
+ const colors = Object.fromEntries(Object.entries(record(body["colors"])).filter((entry) => typeof entry[1] === "string"));
426
+ const fonts = record(body["fonts"]);
427
+ const logo = record(body["logo"]);
428
+ return {
429
+ version: typeof body["version"] === "number" ? body["version"] : empty.version,
430
+ name: text(body["name"]),
431
+ colors,
432
+ fonts: { heading: text(fonts["heading"]), body: text(fonts["body"]) },
433
+ font_size: text(body["font_size"]),
434
+ logo: { primary: text(logo["primary"]), inverse: text(logo["inverse"]), mark: text(logo["mark"]) },
435
+ legal_footer: text(body["legal_footer"])
254
436
  };
255
437
  }
438
+ function readBrand(project) {
439
+ const path = brandPath(project);
440
+ if (!(0, import_node_fs3.existsSync)(path)) return (0, import_engine3.emptyBrand)();
441
+ try {
442
+ return brandFromJson(JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8")));
443
+ } catch (e) {
444
+ throw new DevkitError(`.formfeed/brand.json is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`, "validation");
445
+ }
446
+ }
447
+ function writeBrand(project, brand) {
448
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.join)(project.root, ".formfeed"), { recursive: true });
449
+ const path = brandPath(project);
450
+ (0, import_node_fs3.writeFileSync)(path, JSON.stringify(brand, null, 2) + "\n");
451
+ return path;
452
+ }
256
453
 
257
454
  // packages/devkit/src/lib/local-render.ts
258
- var import_engine = require("@formfeed/engine");
455
+ var import_engine4 = require("@formfeed/engine");
259
456
  function diagnose(tpl, sampleData) {
260
- const engine = (0, import_engine.getEngine)(tpl.meta.engine);
457
+ if (tpl.file) return diagnoseOffice(tpl, tpl.file.bytes, sampleData);
458
+ const engine = (0, import_engine4.getEngine)(tpl.meta.engine);
261
459
  const diagnostics = [];
262
460
  try {
263
461
  engine.compile(tpl.html, { name: tpl.slug });
264
462
  } catch (e) {
265
- if (e instanceof import_engine.EngineSyntaxError) {
463
+ if (e instanceof import_engine4.EngineSyntaxError) {
266
464
  diagnostics.push({
267
465
  severity: "error",
268
466
  code: "syntax-error",
@@ -279,62 +477,785 @@ function diagnose(tpl, sampleData) {
279
477
  if (!source) continue;
280
478
  for (const d of engine.analyze(source, { sampleData }).diagnostics) diagnostics.push({ ...d, message: `${part}: ${d.message}` });
281
479
  }
480
+ for (const name of tpl.missingPartials)
481
+ diagnostics.push({
482
+ severity: "error",
483
+ code: "missing-partial",
484
+ message: `No partial "${name}" in the project's partials folder`,
485
+ range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }
486
+ });
282
487
  return diagnostics;
283
488
  }
284
- function renderContext(project, tpl) {
285
- const settings = (0, import_engine.mergeSettings)(tpl.settings);
489
+ var nowhere = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };
490
+ function diagnoseOffice(tpl, file, sampleData) {
491
+ let analysis;
492
+ try {
493
+ analysis = (0, import_engine4.analyzeOffice)(file, { engine: tpl.meta.engine, sampleData });
494
+ } catch (e) {
495
+ const code = e.code;
496
+ return [{ severity: "error", code: typeof code === "string" ? code : "office-document-invalid", message: e instanceof Error ? e.message : String(e), range: nowhere }];
497
+ }
498
+ return analysis.diagnostics.map((d) => ({
499
+ severity: d.severity,
500
+ code: d.code,
501
+ message: d.text ? `${d.message} (${d.text})` : d.message,
502
+ range: nowhere,
503
+ part: d.part,
504
+ paragraph: d.paragraph
505
+ }));
506
+ }
507
+ async function renderOfficeLocal(project, tpl, data, options = {}) {
508
+ if (!tpl.file) throw new DevkitError(`${tpl.slug} is not a Word or PowerPoint template`, "validation");
509
+ const context = renderContext(project, tpl, options.assetBaseUrl, options.brand);
510
+ if (options.locale) context.locale = options.locale;
511
+ return (0, import_engine4.renderOffice)(tpl.file.bytes, { engine: tpl.meta.engine, data, context, images: options.images, random: options.random });
512
+ }
513
+ function officeSnapshot(filled) {
514
+ return (0, import_engine4.officeTextParts)(filled).map((part) => `--- ${part.name} ---
515
+ ${prettyXml(part.xml)}`).join("\n");
516
+ }
517
+ function prettyXml(xml) {
518
+ const lines = [];
519
+ let depth = 0;
520
+ let open = false;
521
+ for (const token of xml.match(/<[^>]*>|[^<]+/g) ?? []) {
522
+ if (token.startsWith("</")) {
523
+ depth = Math.max(0, depth - 1);
524
+ if (open && lines.length) lines[lines.length - 1] += token;
525
+ else lines.push(" ".repeat(depth) + token);
526
+ open = false;
527
+ } else if (token.startsWith("<?") || token.startsWith("<!")) {
528
+ lines.push(" ".repeat(depth) + token);
529
+ open = false;
530
+ } else if (token.startsWith("<")) {
531
+ lines.push(" ".repeat(depth) + token);
532
+ open = !token.endsWith("/>");
533
+ if (open) depth++;
534
+ } else if (token.trim() || open) {
535
+ if (lines.length) lines[lines.length - 1] += token;
536
+ else lines.push(token);
537
+ }
538
+ }
539
+ return lines.join("\n") + "\n";
540
+ }
541
+ function renderContext(project, tpl, assetBaseUrl, brand) {
542
+ const settings = (0, import_engine4.mergeSettings)(tpl.settings);
286
543
  return {
287
544
  locale: settings.locale ?? "en",
288
545
  timezone: settings.timezone ?? "UTC",
289
546
  currency: settings.currency ?? "EUR",
290
547
  partials: partialResolver(project),
291
- helpers: (0, import_engine.defaultHelpers)(),
292
- limits: import_engine.defaultLimits,
293
- i18n: tpl.i18n ?? void 0
548
+ helpers: (0, import_engine4.defaultHelpers)(),
549
+ limits: import_engine4.defaultLimits,
550
+ i18n: tpl.i18n ?? void 0,
551
+ assetBaseUrl,
552
+ // the organisation's kit from `formfeed brand pull`, else the empty kit the server uses too
553
+ brand: brand ?? readBrand(project)
294
554
  };
295
555
  }
296
556
  async function renderLocal(project, tpl, data, options) {
297
- const ctx = renderContext(project, tpl);
557
+ const kind = htmlKind(tpl);
558
+ const ctx = renderContext(project, tpl, options.assetBaseUrl, options.brand);
298
559
  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 },
560
+ return (0, import_engine4.renderVersion)(
561
+ { engine: tpl.meta.engine, html: tpl.html, css: tpl.css, head: tpl.head, settings: (0, import_engine4.mergeSettings)(tpl.settings), kind },
301
562
  data,
302
563
  ctx,
303
564
  options.mode,
304
565
  options.vendor ? { vendor: options.vendor } : {}
305
566
  );
306
567
  }
568
+ function htmlKind(tpl) {
569
+ if ((0, import_engine4.isOfficeKind)(tpl.meta.kind)) throw new DevkitError(`${tpl.slug} is a ${tpl.meta.kind} template; fill it with renderOfficeLocal`, "validation");
570
+ return tpl.meta.kind;
571
+ }
307
572
  function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
573
+ const kind = htmlKind(tpl);
308
574
  const draft = {
309
575
  document: rendered.document,
310
576
  headerHtml: rendered.headerHtml,
311
577
  footerHtml: rendered.footerHtml,
312
578
  settings: rendered.settings,
313
- kind: tpl.meta.kind
579
+ kind
314
580
  };
315
- return mode === "paged" && tpl.meta.kind === "pdf" ? (0, import_engine.pagedDocument)(draft, { pagedScriptUrl }) : (0, import_engine.flowDocument)(draft);
581
+ return mode === "paged" && kind === "pdf" ? (0, import_engine4.pagedDocument)(draft, { pagedScriptUrl }) : (0, import_engine4.flowDocument)(draft);
582
+ }
583
+
584
+ // packages/devkit/src/lib/files.ts
585
+ var import_node_crypto2 = require("node:crypto");
586
+ var import_node_fs4 = require("node:fs");
587
+ var import_node_path4 = require("node:path");
588
+
589
+ // packages/api-types/src/lib/assets.ts
590
+ var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
591
+ var ASSET_NAME_PATTERN = new RegExp(`^${SEGMENT}(?:/${SEGMENT})*$`);
592
+ var ASSET_MAX_BYTES = 10 * 1024 * 1024;
593
+ var ASSET_CONTENT_TYPES = {
594
+ "image/png": "png",
595
+ "image/jpeg": "jpg",
596
+ "image/webp": "webp",
597
+ "image/gif": "gif",
598
+ "image/svg+xml": "svg",
599
+ "image/avif": "avif",
600
+ "application/pdf": "pdf"
601
+ };
602
+ function isAssetName(name) {
603
+ return name.length > 0 && name.length <= 200 && ASSET_NAME_PATTERN.test(name) && !name.split("/").includes("..");
604
+ }
605
+
606
+ // packages/devkit/src/lib/files.ts
607
+ var byExtension = Object.fromEntries(
608
+ Object.entries(ASSET_CONTENT_TYPES).map(([type, ext]) => [ext, type])
609
+ );
610
+ byExtension["jpeg"] = "image/jpeg";
611
+ function contentTypeFor(name) {
612
+ return byExtension[name.split(".").pop()?.toLowerCase() ?? ""] ?? null;
613
+ }
614
+ function sha256File(bytes) {
615
+ return (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex");
616
+ }
617
+ function listLocalFiles(project) {
618
+ const listing = { files: [], skipped: [] };
619
+ if (!(0, import_node_fs4.existsSync)(project.filesDir)) return listing;
620
+ const walk2 = (dir) => {
621
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
622
+ if (entry.name.startsWith(".")) continue;
623
+ const path = (0, import_node_path4.join)(dir, entry.name);
624
+ if (entry.isDirectory()) {
625
+ walk2(path);
626
+ continue;
627
+ }
628
+ if (!entry.isFile()) continue;
629
+ const name = (0, import_node_path4.relative)(project.filesDir, path).split(import_node_path4.sep).join("/");
630
+ const size = (0, import_node_fs4.statSync)(path).size;
631
+ const contentType = contentTypeFor(name);
632
+ const reason = !isAssetName(name) ? "the name has characters a library name cannot have (letters, digits, dot, dash, underscore)" : !contentType ? "not an image or a PDF" : size === 0 ? "empty" : size > ASSET_MAX_BYTES ? `larger than ${ASSET_MAX_BYTES / 1024 / 1024} MB` : null;
633
+ if (reason) {
634
+ listing.skipped.push({ path, reason });
635
+ continue;
636
+ }
637
+ const bytes = (0, import_node_fs4.readFileSync)(path);
638
+ listing.files.push({ name, path, bytes: size, sha256: sha256File(bytes), contentType });
639
+ }
640
+ };
641
+ walk2(project.filesDir);
642
+ listing.files.sort((a, b) => a.name.localeCompare(b.name));
643
+ return listing;
644
+ }
645
+ function localFilePath(project, name) {
646
+ if (!isAssetName(name)) throw new DevkitError(`"${name}" is not a library file name`);
647
+ const path = (0, import_node_path4.resolve)(project.filesDir, ...name.split("/"));
648
+ if (!path.startsWith((0, import_node_path4.resolve)(project.filesDir) + import_node_path4.sep)) throw new DevkitError(`"${name}" is not inside the files folder`);
649
+ return path;
650
+ }
651
+ function writeLocalFile(project, name, bytes) {
652
+ const path = localFilePath(project, name);
653
+ (0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
654
+ (0, import_node_fs4.writeFileSync)(path, bytes);
655
+ return path;
656
+ }
657
+
658
+ // packages/devkit/src/lib/redact.ts
659
+ function redactData(value, paths) {
660
+ if (!paths || paths.length === 0) return redactAll(value);
661
+ return walk(value, paths.map(parseRedactPath));
662
+ }
663
+ function redactString(text2) {
664
+ let out = "";
665
+ for (const ch of text2) {
666
+ if (/\p{Lu}/u.test(ch)) out += "A";
667
+ else if (/\p{L}/u.test(ch)) out += "a";
668
+ else if (/\p{N}/u.test(ch)) out += "0";
669
+ else out += ch;
670
+ }
671
+ return out;
672
+ }
673
+ function parseRedactPath(path) {
674
+ const segments = [];
675
+ for (const part of path.trim().split(".")) {
676
+ if (part === "") continue;
677
+ const match = /^([^[\]]*)((?:\[[^\]]*\])*)$/.exec(part);
678
+ if (!match) {
679
+ segments.push(part);
680
+ continue;
681
+ }
682
+ if (match[1]) segments.push(match[1]);
683
+ for (const index of match[2].match(/\[[^\]]*\]/g) ?? []) {
684
+ const inner = index.slice(1, -1).trim();
685
+ segments.push(inner === "" || inner === "*" ? "*" : inner);
686
+ }
687
+ }
688
+ return segments;
689
+ }
690
+ function redactAll(value) {
691
+ if (typeof value === "string") return redactString(value);
692
+ if (Array.isArray(value)) return value.map(redactAll);
693
+ if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactAll(v)]));
694
+ return value;
695
+ }
696
+ function walk(value, patterns) {
697
+ if (patterns.some((p) => p.length === 0)) return redactAll(value);
698
+ if (Array.isArray(value)) {
699
+ return value.map((item, index) => {
700
+ const next = advance(patterns, String(index));
701
+ return next.length ? walk(item, next) : item;
702
+ });
703
+ }
704
+ if (isRecord(value)) {
705
+ return Object.fromEntries(
706
+ Object.entries(value).map(([key, item]) => {
707
+ const next = advance(patterns, key);
708
+ return [key, next.length ? walk(item, next) : item];
709
+ })
710
+ );
711
+ }
712
+ return value;
713
+ }
714
+ function advance(patterns, key) {
715
+ return patterns.filter((p) => p[0] === "*" || p[0] === key).map((p) => p.slice(1));
716
+ }
717
+ function isRecord(value) {
718
+ return typeof value === "object" && value !== null && !Array.isArray(value);
719
+ }
720
+
721
+ // packages/devkit/src/lib/types/index.ts
722
+ var import_engine5 = require("@formfeed/engine");
723
+
724
+ // packages/devkit/src/lib/types/model.ts
725
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
726
+ var UNSUPPORTED = [
727
+ "not",
728
+ "if",
729
+ "then",
730
+ "else",
731
+ "patternProperties",
732
+ "prefixItems",
733
+ "contains",
734
+ "dependentSchemas",
735
+ "dependentRequired",
736
+ "unevaluatedProperties",
737
+ "unevaluatedItems",
738
+ "propertyNames"
739
+ ];
740
+ function pascalCase(text2) {
741
+ const words = text2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
742
+ const joined = words.map((w) => w[0].toUpperCase() + w.slice(1)).join("");
743
+ if (!joined) return "Field";
744
+ return /^[0-9]/.test(joined) ? `T${joined}` : joined;
745
+ }
746
+ function singular(word) {
747
+ if (/ies$/i.test(word) && word.length > 3) return word.slice(0, -3) + "y";
748
+ if (/(ss|x|ch|sh)es$/i.test(word)) return word.slice(0, -2);
749
+ if (/[^su]s$/i.test(word)) return word.slice(0, -1);
750
+ return null;
751
+ }
752
+ var NameRegistry = class {
753
+ used = /* @__PURE__ */ new Set();
754
+ constructor(reserved = []) {
755
+ for (const name of reserved) this.used.add(name);
756
+ }
757
+ claim(wanted) {
758
+ let name = wanted;
759
+ for (let i = 2; this.used.has(name); i++) name = `${wanted}${i}`;
760
+ this.used.add(name);
761
+ return name;
762
+ }
763
+ };
764
+ function relaxRequired(schema) {
765
+ if (Array.isArray(schema)) return schema.map(relaxRequired);
766
+ if (!isObject(schema)) return schema;
767
+ const out = {};
768
+ for (const [key, value] of Object.entries(schema)) {
769
+ if (key === "required") continue;
770
+ if (key === "properties" || key === "$defs" || key === "definitions") {
771
+ out[key] = isObject(value) ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, relaxRequired(v)])) : value;
772
+ } else if (key === "examples" || key === "enum" || key === "const" || key === "default") {
773
+ out[key] = value;
774
+ } else {
775
+ out[key] = relaxRequired(value);
776
+ }
777
+ }
778
+ return out;
779
+ }
780
+ function docOf(schema) {
781
+ const lines = [];
782
+ if (typeof schema["title"] === "string" && schema["title"] !== schema["description"]) lines.push(schema["title"]);
783
+ if (typeof schema["description"] === "string") lines.push(...schema["description"].split("\n"));
784
+ if (typeof schema["format"] === "string") lines.push(`Format: ${schema["format"]}`);
785
+ const unsupported = UNSUPPORTED.filter((k) => k in schema);
786
+ if (unsupported.length) lines.push(`Not expressed in the types: ${unsupported.join(", ")}`);
787
+ if (Array.isArray(schema["examples"]) && schema["examples"].length)
788
+ lines.push(`@example ${JSON.stringify(schema["examples"][0])}`);
789
+ if (schema["deprecated"] === true) lines.push("@deprecated");
790
+ return lines;
791
+ }
792
+ var Builder = class {
793
+ constructor(root, base, names) {
794
+ this.root = root;
795
+ this.base = base;
796
+ this.names = names;
797
+ }
798
+ objects = [];
799
+ defs = /* @__PURE__ */ new Map();
800
+ rootName = "";
801
+ /**
802
+ * A plain object schema becomes the interface `rootName` itself; anything else (a nullable object,
803
+ * a union, an array) becomes an alias of that name, so the root never names two things.
804
+ */
805
+ build(rootName) {
806
+ this.rootName = rootName;
807
+ const s = this.root;
808
+ const plainObject = (s["type"] === "object" || s["type"] === void 0 && isObject(s["properties"])) && isObject(s["properties"]) && Object.keys(s["properties"]).length > 0 && !["$ref", "const", "enum", "anyOf", "oneOf", "allOf"].some((k) => k in s);
809
+ return this.convert(s, null, null, plainObject ? rootName : void 0);
810
+ }
811
+ /**
812
+ * `owner` + `key` name the objects found below: property `customer` of `Invoice` is
813
+ * `InvoiceCustomer`, an element of `items` is `InvoiceItem`. `exact` forces a name (the root).
814
+ */
815
+ convert(raw, owner, key, exact, depth = 0) {
816
+ if (raw === true || raw === void 0) return { kind: "unknown" };
817
+ if (raw === false) return { kind: "never" };
818
+ if (!isObject(raw) || depth > 48) return { kind: "unknown" };
819
+ const schema = raw;
820
+ if (typeof schema["$ref"] === "string") return this.reference(schema["$ref"]);
821
+ if ("const" in schema) return literal(schema["const"]);
822
+ if (Array.isArray(schema["enum"])) {
823
+ const members2 = schema["enum"].map(literal);
824
+ return members2.length === 1 ? members2[0] : { kind: "union", members: members2 };
825
+ }
826
+ for (const combinator of ["anyOf", "oneOf"]) {
827
+ const list = schema[combinator];
828
+ if (Array.isArray(list) && list.length) {
829
+ const members2 = list.map(
830
+ (option, i) => this.convert(option, owner, key === null ? `Option${i + 1}` : `${key}Option${i + 1}`, void 0, depth + 1)
831
+ );
832
+ return union(members2);
833
+ }
834
+ }
835
+ if (Array.isArray(schema["allOf"]) && schema["allOf"].length) {
836
+ const members2 = schema["allOf"].map(
837
+ (part, i) => this.convert(part, owner, key === null ? `Part${i + 1}` : `${key}Part${i + 1}`, void 0, depth + 1)
838
+ );
839
+ return members2.length === 1 ? members2[0] : { kind: "intersection", members: members2 };
840
+ }
841
+ const declared = typeof schema["type"] === "string" ? [schema["type"]] : Array.isArray(schema["type"]) ? schema["type"] : null;
842
+ const types = declared ?? (isObject(schema["properties"]) || isObject(schema["additionalProperties"]) ? ["object"] : "items" in schema ? ["array"] : null);
843
+ if (!types) {
844
+ const unsupported = UNSUPPORTED.filter((k) => k in schema);
845
+ return unsupported.length ? { kind: "unknown", note: unsupported.join(", ") } : { kind: "unknown" };
846
+ }
847
+ const members = types.map((type) => {
848
+ switch (type) {
849
+ case "object":
850
+ return this.object(schema, owner, key, exact, depth);
851
+ case "array":
852
+ return { kind: "array", items: this.convert(schema["items"], owner, itemKey(key), void 0, depth + 1) };
853
+ case "string":
854
+ case "number":
855
+ case "integer":
856
+ case "boolean":
857
+ case "null":
858
+ return { kind: "primitive", name: type };
859
+ default:
860
+ return { kind: "unknown" };
861
+ }
862
+ });
863
+ return union(members);
864
+ }
865
+ object(schema, owner, key, exact, depth) {
866
+ const properties = isObject(schema["properties"]) ? schema["properties"] : {};
867
+ const additional = schema["additionalProperties"];
868
+ const extra = isObject(additional) ? this.convert(additional, owner, `${key ?? ""}Value`, void 0, depth + 1) : null;
869
+ if (Object.keys(properties).length === 0) return { kind: "record", values: extra ?? { kind: "unknown" } };
870
+ const name = exact ?? this.names.claim(`${owner ?? this.base}${key === null ? "" : pascalCase(key)}`);
871
+ const target = { name, doc: docOf(schema), fields: [], extra };
872
+ this.objects.push(target);
873
+ const required = new Set(Array.isArray(schema["required"]) ? schema["required"] : []);
874
+ const nestedOwner = name === this.rootName ? this.base : name;
875
+ for (const [field, child] of Object.entries(properties)) {
876
+ target.fields.push({
877
+ key: field,
878
+ type: this.convert(child, nestedOwner, field, void 0, depth + 1),
879
+ required: required.has(field),
880
+ doc: isObject(child) ? docOf(child) : []
881
+ });
882
+ }
883
+ return { kind: "ref", name };
884
+ }
885
+ reference(ref) {
886
+ const match = /^#\/(\$defs|definitions)\/(.+)$/.exec(ref);
887
+ if (!match) return { kind: "unknown", note: `$ref ${ref}` };
888
+ const known = this.defs.get(ref);
889
+ if (known) return { kind: "ref", name: known };
890
+ const section = this.root[match[1]];
891
+ const target = isObject(section) ? section[decodePointer(match[2])] : void 0;
892
+ if (!isObject(target)) return { kind: "unknown", note: `$ref ${ref}` };
893
+ const hasProperties = isObject(target["properties"]) && Object.keys(target["properties"]).length > 0;
894
+ if (!hasProperties) return this.convert(target, this.base, decodePointer(match[2]));
895
+ const name = this.names.claim(`${this.base}${pascalCase(decodePointer(match[2]))}`);
896
+ this.defs.set(ref, name);
897
+ this.convert(target, null, null, name);
898
+ return { kind: "ref", name };
899
+ }
900
+ };
901
+ var decodePointer = (part) => part.replace(/~1/g, "/").replace(/~0/g, "~");
902
+ function itemKey(key) {
903
+ if (key === null) return "Item";
904
+ return singular(key) ?? `${key}Item`;
905
+ }
906
+ function literal(value) {
907
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean")
908
+ return { kind: "literal", value };
909
+ return { kind: "unknown" };
910
+ }
911
+ function union(members) {
912
+ const flat = [];
913
+ for (const member of members) {
914
+ if (member.kind === "union") flat.push(...member.members);
915
+ else flat.push(member);
916
+ }
917
+ if (flat.some((m) => m.kind === "unknown")) return { kind: "unknown" };
918
+ const unique = flat.filter((m, i) => flat.findIndex((n) => JSON.stringify(n) === JSON.stringify(m)) === i);
919
+ return unique.length === 1 ? unique[0] : { kind: "union", members: unique };
920
+ }
921
+ function buildTypes(sources, reserved = []) {
922
+ const names = new NameRegistry(reserved);
923
+ return [...sources].sort((a, b) => a.slug.localeCompare(b.slug)).map((source) => {
924
+ const schema = source.origin === "inferred" ? relaxRequired(source.schema) : source.schema;
925
+ const base = pascalCase(source.slug);
926
+ const rootName = names.claim(`${base}Data`);
927
+ const builder = new Builder(schema, base, names);
928
+ const root = builder.build(rootName);
929
+ return {
930
+ slug: source.slug,
931
+ origin: source.origin,
932
+ version: source.version ?? null,
933
+ channel: source.channel ?? null,
934
+ rootName,
935
+ root,
936
+ objects: builder.objects
937
+ };
938
+ });
939
+ }
940
+ function originNote(t) {
941
+ switch (t.origin) {
942
+ case "local":
943
+ return "schema.json of the project";
944
+ case "stored":
945
+ return `stored schema of version ${t.version ?? "?"}${t.channel ? ` (channel ${t.channel})` : ""}`;
946
+ case "inferred":
947
+ return "inferred from sample data, every field optional: store a schema to make this a contract";
948
+ }
949
+ }
950
+
951
+ // packages/devkit/src/lib/types/emit-python.ts
952
+ var KEYWORDS = /* @__PURE__ */ new Set([
953
+ "False",
954
+ "None",
955
+ "True",
956
+ "and",
957
+ "as",
958
+ "assert",
959
+ "async",
960
+ "await",
961
+ "break",
962
+ "class",
963
+ "continue",
964
+ "def",
965
+ "del",
966
+ "elif",
967
+ "else",
968
+ "except",
969
+ "finally",
970
+ "for",
971
+ "from",
972
+ "global",
973
+ "if",
974
+ "import",
975
+ "in",
976
+ "is",
977
+ "lambda",
978
+ "nonlocal",
979
+ "not",
980
+ "or",
981
+ "pass",
982
+ "raise",
983
+ "return",
984
+ "try",
985
+ "while",
986
+ "with",
987
+ "yield"
988
+ ]);
989
+ var isIdentifier = (key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) && !KEYWORDS.has(key);
990
+ function methodName(slug) {
991
+ const name = slug.replace(/[^A-Za-z0-9_]+/g, "_").toLowerCase();
992
+ return isIdentifier(name) ? name : `${/^[0-9]/.test(name) ? "t_" : ""}${name}${KEYWORDS.has(name) ? "_" : ""}`;
993
+ }
994
+ function expression(node, quoteRefs) {
995
+ switch (node.kind) {
996
+ case "unknown":
997
+ return "Any";
998
+ case "never":
999
+ return "Never";
1000
+ case "primitive":
1001
+ return { string: "str", number: "float", integer: "int", boolean: "bool", null: "None" }[node.name];
1002
+ case "literal":
1003
+ return node.value === null ? "None" : `Literal[${pythonLiteral(node.value)}]`;
1004
+ case "array":
1005
+ return `list[${expression(node.items, quoteRefs)}]`;
1006
+ case "record":
1007
+ return `dict[str, ${expression(node.values, quoteRefs)}]`;
1008
+ case "ref":
1009
+ return quoteRefs ? JSON.stringify(node.name) : node.name;
1010
+ case "union": {
1011
+ const literals = node.members.filter(
1012
+ (m) => m.kind === "literal" && m.value !== null
1013
+ );
1014
+ const rest = node.members.filter((m) => !(m.kind === "literal" && m.value !== null));
1015
+ const parts = [
1016
+ ...literals.length ? [`Literal[${literals.map((l) => pythonLiteral(l.value)).join(", ")}]`] : [],
1017
+ ...rest.map((m) => expression(m, quoteRefs))
1018
+ ];
1019
+ if (quoteRefs && rest.some((m) => m.kind === "ref")) return `Union[${parts.join(", ")}]`;
1020
+ return parts.join(" | ");
1021
+ }
1022
+ case "intersection":
1023
+ return expression(node.members[0] ?? { kind: "unknown" }, quoteRefs);
1024
+ }
1025
+ }
1026
+ function pythonLiteral(value) {
1027
+ if (value === null) return "None";
1028
+ if (typeof value === "boolean") return value ? "True" : "False";
1029
+ return JSON.stringify(value);
1030
+ }
1031
+ function docstring(lines, indent) {
1032
+ if (lines.length === 0) return [];
1033
+ const safe = lines.map((line) => line.replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"'));
1034
+ if (safe.length === 1) return [`${indent}"""${safe[0]}"""`];
1035
+ return [`${indent}"""${safe[0]}`, ...safe.slice(1).map((line) => `${indent}${line}`.trimEnd()), `${indent}"""`];
1036
+ }
1037
+ function emitObject(object) {
1038
+ const functional = object.fields.some((f) => !isIdentifier(f.key));
1039
+ const field = (type, required, quote) => {
1040
+ const text2 = expression(type, quote);
1041
+ return required ? text2 : `NotRequired[${text2}]`;
1042
+ };
1043
+ if (functional) {
1044
+ const out2 = [`${object.name} = TypedDict(`, ` ${JSON.stringify(object.name)},`, " {"];
1045
+ for (const f of object.fields) out2.push(` ${JSON.stringify(f.key)}: ${field(f.type, f.required, true)},`);
1046
+ out2.push(" },", ")");
1047
+ if (object.doc.length) out2.unshift(...object.doc.map((line) => `# ${line}`));
1048
+ return out2;
1049
+ }
1050
+ const out = [`class ${object.name}(TypedDict):`, ...docstring(object.doc, " ")];
1051
+ for (const f of object.fields) {
1052
+ out.push(` ${f.key}: ${field(f.type, f.required, false)}`);
1053
+ out.push(...docstring(f.doc, " "));
1054
+ }
1055
+ if (object.fields.length === 0) out.push(" pass");
1056
+ return out;
1057
+ }
1058
+ function emitPython(templates) {
1059
+ const lines = [
1060
+ "# Generated by `formfeed types`. Do not edit: run it again when a template schema changes.",
1061
+ "#",
1062
+ ...templates.map((t) => `# ${t.slug}: ${originNote(t)}`),
1063
+ "",
1064
+ "from __future__ import annotations",
1065
+ "",
1066
+ "from typing import Any, Literal, Union, cast",
1067
+ "",
1068
+ "from typing_extensions import Never, NotRequired, TypedDict",
1069
+ "",
1070
+ "from formfeed import AsyncFormfeed, Formfeed, Render",
1071
+ "",
1072
+ "__all__ = [",
1073
+ ...templates.map((t) => ` ${JSON.stringify(t.rootName)},`),
1074
+ ' "TypedRenders",',
1075
+ ' "AsyncTypedRenders",',
1076
+ ' "typed",',
1077
+ ' "typed_async",',
1078
+ "]"
1079
+ ];
1080
+ for (const template of templates) {
1081
+ lines.push("", "", `# --- ${template.slug}`);
1082
+ for (const object of [...template.objects].reverse()) lines.push("", ...emitObject(object));
1083
+ const isClass = template.root.kind === "ref" && template.root.name === template.rootName;
1084
+ if (!isClass) lines.push("", `${template.rootName} = ${expression(template.root, true)}`);
1085
+ }
1086
+ const wrapper = (name, isAsync) => {
1087
+ const example = templates[0] ? methodName(templates[0].slug) : "invoice";
1088
+ const out = [
1089
+ "",
1090
+ "",
1091
+ `class ${name}:`,
1092
+ ` """Renders with \`data\` checked per template: \`${isAsync ? "await typed_async(client)" : "typed(client)"}.${example}(data, **options)\`."""`,
1093
+ "",
1094
+ ` def __init__(self, client: ${isAsync ? "AsyncFormfeed" : "Formfeed"}) -> None:`,
1095
+ " self._renders: Any = client.renders"
1096
+ ];
1097
+ for (const t of templates) {
1098
+ out.push(
1099
+ "",
1100
+ ` ${isAsync ? "async " : ""}def ${methodName(t.slug)}(self, data: ${t.rootName}, **options: Any) -> Render:`,
1101
+ ` return cast(Render, ${isAsync ? "await " : ""}self._renders.create(template=${JSON.stringify(t.slug)}, data=data, **options))`
1102
+ );
1103
+ }
1104
+ return out;
1105
+ };
1106
+ lines.push(...wrapper("TypedRenders", false), ...wrapper("AsyncTypedRenders", true));
1107
+ lines.push(
1108
+ "",
1109
+ "",
1110
+ "def typed(client: Formfeed) -> TypedRenders:",
1111
+ " return TypedRenders(client)",
1112
+ "",
1113
+ "",
1114
+ "def typed_async(client: AsyncFormfeed) -> AsyncTypedRenders:",
1115
+ " return AsyncTypedRenders(client)"
1116
+ );
1117
+ return lines.join("\n") + "\n";
1118
+ }
1119
+
1120
+ // packages/devkit/src/lib/types/emit-ts.ts
1121
+ var identifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1122
+ var quoteKey = (key) => identifier.test(key) ? key : JSON.stringify(key);
1123
+ function expression2(node, nested = false) {
1124
+ switch (node.kind) {
1125
+ case "unknown":
1126
+ return "unknown";
1127
+ case "never":
1128
+ return "never";
1129
+ case "primitive":
1130
+ return node.name === "integer" ? "number" : node.name;
1131
+ case "literal":
1132
+ return JSON.stringify(node.value);
1133
+ case "array": {
1134
+ const inner = expression2(node.items, true);
1135
+ return /[|&]/.test(inner) ? `Array<${expression2(node.items)}>` : `${inner}[]`;
1136
+ }
1137
+ case "record":
1138
+ return `Record<string, ${expression2(node.values)}>`;
1139
+ case "ref":
1140
+ return node.name;
1141
+ case "union": {
1142
+ const text2 = node.members.map((m) => expression2(m, true)).join(" | ");
1143
+ return nested ? `(${text2})` : text2;
1144
+ }
1145
+ case "intersection": {
1146
+ const text2 = node.members.map((m) => expression2(m, true)).join(" & ");
1147
+ return nested ? `(${text2})` : text2;
1148
+ }
1149
+ }
1150
+ }
1151
+ function docBlock(lines, indent) {
1152
+ if (lines.length === 0) return [];
1153
+ const safe = lines.map((line) => line.replace(/\*\//g, "*\\/"));
1154
+ if (safe.length === 1) return [`${indent}/** ${safe[0]} */`];
1155
+ return [`${indent}/**`, ...safe.map((line) => `${indent} * ${line}`.trimEnd()), `${indent} */`];
1156
+ }
1157
+ function emitObject2(object) {
1158
+ const out = [...docBlock(object.doc, ""), `export interface ${object.name} {`];
1159
+ for (const field of object.fields) {
1160
+ out.push(...docBlock(field.doc, " "));
1161
+ out.push(` ${quoteKey(field.key)}${field.required ? "" : "?"}: ${expression2(field.type)};`);
1162
+ }
1163
+ if (object.extra) {
1164
+ out.push(` /** Further fields: ${expression2(object.extra).replace(/\*\//g, "*\\/")} */`);
1165
+ out.push(" [key: string]: unknown;");
1166
+ }
1167
+ out.push("}");
1168
+ return out;
1169
+ }
1170
+ function emitTypeScript(templates, options = {}) {
1171
+ const sdk = options.sdkModule ?? "@formfeed/sdk";
1172
+ const lines = [
1173
+ "// Generated by `formfeed types`. Do not edit: run it again when a template schema changes.",
1174
+ "//",
1175
+ ...templates.map((t) => `// ${t.slug}: ${originNote(t)}`),
1176
+ "",
1177
+ `declare module ${JSON.stringify(sdk)} {`,
1178
+ " interface FormfeedTemplates {",
1179
+ ...templates.map((t) => ` ${JSON.stringify(t.slug)}: ${t.rootName};`),
1180
+ " }",
1181
+ "}"
1182
+ ];
1183
+ for (const template of templates) {
1184
+ lines.push("", `// --- ${template.slug}`);
1185
+ const isInterface = template.root.kind === "ref" && template.root.name === template.rootName;
1186
+ if (!isInterface) lines.push("", `export type ${template.rootName} = ${expression2(template.root)};`);
1187
+ for (const object of template.objects) lines.push("", ...emitObject2(object));
1188
+ }
1189
+ if (templates.length === 0) lines.push("", "export {};");
1190
+ return lines.join("\n") + "\n";
1191
+ }
1192
+
1193
+ // packages/devkit/src/lib/types/index.ts
1194
+ var defaultTypesFile = {
1195
+ ts: "formfeed.d.ts",
1196
+ python: "formfeed_templates.py"
1197
+ };
1198
+ function localTypeSource(tpl) {
1199
+ if (tpl.dataSchema) return { slug: tpl.slug, schema: tpl.dataSchema, origin: "local" };
1200
+ const sets = Object.values(tpl.dataSets);
1201
+ return {
1202
+ slug: tpl.slug,
1203
+ schema: (0, import_engine5.inferSchemaFromDataSets)(sets.length ? sets : [{}]),
1204
+ origin: "inferred"
1205
+ };
1206
+ }
1207
+ function generateTypes(sources, language, options = {}) {
1208
+ const reserved = language === "python" ? ["TypedRenders", "AsyncTypedRenders", "Formfeed", "AsyncFormfeed", "Render", "Any", "Literal", "Union", "Never", "NotRequired", "TypedDict"] : ["FormfeedTemplates"];
1209
+ const model = buildTypes(sources, reserved);
1210
+ return language === "python" ? emitPython(model) : emitTypeScript(model, options);
316
1211
  }
317
1212
  // Annotate the CommonJS export names for ESM import in node:
318
1213
  0 && (module.exports = {
319
1214
  DevkitError,
1215
+ brandFromJson,
1216
+ brandPath,
1217
+ collectPartials,
320
1218
  contentHash,
1219
+ contentTypeFor,
321
1220
  defaultData,
322
1221
  defaultProjectConfig,
1222
+ defaultTypesFile,
323
1223
  diagnose,
324
1224
  findProject,
1225
+ generateTypes,
1226
+ listLocalFiles,
325
1227
  listTemplateSlugs,
326
1228
  loadProject,
1229
+ localFilePath,
1230
+ localTypeSource,
1231
+ officeSnapshot,
1232
+ officeVersionPayload,
1233
+ parseRedactPath,
1234
+ partialContentHash,
327
1235
  partialResolver,
1236
+ prettyXml,
328
1237
  previewDocument,
329
1238
  projectAround,
1239
+ readBrand,
330
1240
  readState,
331
1241
  readTemplate,
1242
+ recordSharedPartial,
332
1243
  recordSync,
1244
+ redactData,
1245
+ redactString,
333
1246
  renderContext,
334
1247
  renderLocal,
1248
+ renderOfficeLocal,
1249
+ sha256File,
1250
+ sharedPartialNames,
1251
+ sharedPartialPath,
335
1252
  templateDir,
1253
+ templateFileName,
336
1254
  titleFromSlug,
337
1255
  versionPayload,
1256
+ writeBrand,
1257
+ writeLocalFile,
1258
+ writePartials,
338
1259
  writeProjectConfig,
339
1260
  writeState,
340
1261
  writeTemplate