@formfeed/devkit 0.2.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,29 +21,48 @@ 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,
24
26
  collectPartials: () => collectPartials,
25
27
  contentHash: () => contentHash,
26
28
  contentTypeFor: () => contentTypeFor,
27
29
  defaultData: () => defaultData,
28
30
  defaultProjectConfig: () => defaultProjectConfig,
31
+ defaultTypesFile: () => defaultTypesFile,
29
32
  diagnose: () => diagnose,
30
33
  findProject: () => findProject,
34
+ generateTypes: () => generateTypes,
31
35
  listLocalFiles: () => listLocalFiles,
32
36
  listTemplateSlugs: () => listTemplateSlugs,
33
37
  loadProject: () => loadProject,
34
38
  localFilePath: () => localFilePath,
39
+ localTypeSource: () => localTypeSource,
40
+ officeSnapshot: () => officeSnapshot,
41
+ officeVersionPayload: () => officeVersionPayload,
42
+ parseRedactPath: () => parseRedactPath,
43
+ partialContentHash: () => partialContentHash,
35
44
  partialResolver: () => partialResolver,
45
+ prettyXml: () => prettyXml,
36
46
  previewDocument: () => previewDocument,
37
47
  projectAround: () => projectAround,
48
+ readBrand: () => readBrand,
38
49
  readState: () => readState,
39
50
  readTemplate: () => readTemplate,
51
+ recordSharedPartial: () => recordSharedPartial,
40
52
  recordSync: () => recordSync,
53
+ redactData: () => redactData,
54
+ redactString: () => redactString,
41
55
  renderContext: () => renderContext,
42
56
  renderLocal: () => renderLocal,
57
+ renderOfficeLocal: () => renderOfficeLocal,
43
58
  sha256File: () => sha256File,
59
+ sharedPartialNames: () => sharedPartialNames,
60
+ sharedPartialPath: () => sharedPartialPath,
44
61
  templateDir: () => templateDir,
62
+ templateFileName: () => templateFileName,
45
63
  titleFromSlug: () => titleFromSlug,
46
64
  versionPayload: () => versionPayload,
65
+ writeBrand: () => writeBrand,
47
66
  writeLocalFile: () => writeLocalFile,
48
67
  writePartials: () => writePartials,
49
68
  writeProjectConfig: () => writeProjectConfig,
@@ -125,6 +144,7 @@ function writeProjectConfig(root, config) {
125
144
  var import_node_crypto = require("node:crypto");
126
145
  var import_node_fs2 = require("node:fs");
127
146
  var import_node_path2 = require("node:path");
147
+ var import_engine2 = require("@formfeed/engine");
128
148
 
129
149
  // packages/devkit/src/lib/partials.ts
130
150
  var import_engine = require("@formfeed/engine");
@@ -137,8 +157,10 @@ function includeNames(engine, source) {
137
157
  }
138
158
  function collectPartials(project, engine, sources) {
139
159
  const resolve3 = partialResolver(project);
160
+ const sharedNames = sharedPartialNames(project, engine);
140
161
  const partials = {};
141
162
  const missing = /* @__PURE__ */ new Set();
163
+ const shared = /* @__PURE__ */ new Set();
142
164
  const queue = sources.filter((source) => Boolean(source));
143
165
  const seen = /* @__PURE__ */ new Set();
144
166
  while (queue.length) {
@@ -146,6 +168,11 @@ function collectPartials(project, engine, sources) {
146
168
  if (seen.has(name)) continue;
147
169
  seen.add(name);
148
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
+ }
149
176
  if (source === void 0) {
150
177
  missing.add(name);
151
178
  continue;
@@ -154,65 +181,86 @@ function collectPartials(project, engine, sources) {
154
181
  queue.push(source);
155
182
  }
156
183
  }
157
- return { partials, missing: [...missing].sort() };
184
+ return { partials, missing: [...missing].sort(), shared: [...shared].sort() };
158
185
  }
159
186
 
160
187
  // packages/devkit/src/lib/project.ts
161
188
  var readText = (path) => (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : null;
162
189
  function readJson(path, what) {
163
- const text = readText(path);
164
- if (text === null) return null;
190
+ const text2 = readText(path);
191
+ if (text2 === null) return null;
165
192
  try {
166
- return JSON.parse(text);
193
+ return JSON.parse(text2);
167
194
  } catch (e) {
168
195
  throw new DevkitError(`${what} is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`, "validation");
169
196
  }
170
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
+ }
171
202
  function listTemplateSlugs(project) {
172
203
  if (!(0, import_node_fs2.existsSync)(project.templatesDir)) return [];
173
- 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();
174
205
  }
175
206
  function templateDir(project, slug) {
176
207
  return (0, import_node_path2.join)(project.templatesDir, slug);
177
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
+ }
178
215
  function readTemplate(project, slug) {
179
216
  const dir = templateDir(project, slug);
180
- const html = readText((0, import_node_path2.join)(dir, "template.html"));
181
- 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")) ?? "";
182
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");
183
229
  const meta = {
184
230
  name: metaFile.name ?? titleFromSlug(slug),
185
- kind: metaFile.kind ?? "pdf",
231
+ kind: file?.format ?? metaFile.kind ?? "pdf",
186
232
  engine: metaFile.engine ?? project.config.engine,
187
233
  description: metaFile.description ?? null,
188
234
  tags: metaFile.tags ?? []
189
235
  };
190
236
  const settings = { ...readJson((0, import_node_path2.join)(dir, "settings.json"), "settings.json") ?? {} };
191
- const header = readText((0, import_node_path2.join)(dir, "header.html"));
192
- 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"));
193
239
  if (header !== null && header.trim()) settings.header = { ...settings.header ?? {}, html: header };
194
240
  if (footer !== null && footer.trim()) settings.footer = { ...settings.footer ?? {}, html: footer };
195
241
  const dataSets = {};
196
242
  const dataDir = (0, import_node_path2.join)(dir, "data");
197
243
  if ((0, import_node_fs2.existsSync)(dataDir)) {
198
- for (const file of (0, import_node_fs2.readdirSync)(dataDir).filter((f) => f.endsWith(".json")).sort()) {
199
- 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}`);
200
246
  }
201
247
  }
202
- const { partials, missing } = collectPartials(project, meta.engine, [html, settings.header?.html, settings.footer?.html]);
248
+ const { partials, missing, shared } = file ? { partials: {}, missing: [], shared: [] } : collectPartials(project, meta.engine, [html, settings.header?.html, settings.footer?.html]);
203
249
  return {
204
250
  slug,
205
251
  dir,
206
252
  meta,
253
+ file,
207
254
  html,
208
- css: readText((0, import_node_path2.join)(dir, "style.css")) ?? "",
209
- 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")) ?? "",
210
257
  settings,
211
258
  dataSets,
212
259
  dataSchema: readJson((0, import_node_path2.join)(dir, "schema.json"), "schema.json"),
213
260
  i18n: readJson((0, import_node_path2.join)(dir, "i18n.json"), "i18n.json"),
214
261
  partials,
215
- missingPartials: missing
262
+ missingPartials: missing,
263
+ sharedPartials: shared
216
264
  };
217
265
  }
218
266
  function defaultData(tpl, name) {
@@ -222,18 +270,23 @@ function defaultData(tpl, name) {
222
270
  throw new DevkitError(`Data set "${name}" not found; available: ${names.join(", ") || "none"}`);
223
271
  return chosen ? { name: chosen, data: tpl.dataSets[chosen] } : { name: "empty", data: {} };
224
272
  }
225
- function writeTemplate(project, slug, meta, version) {
273
+ function writeTemplate(project, slug, meta, version, file) {
226
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`);
227
277
  (0, import_node_fs2.mkdirSync)((0, import_node_path2.join)(dir, "data"), { recursive: true });
228
278
  const settings = { ...version.settings ?? {} };
229
- const header = settings.header?.html ?? "";
230
- const footer = settings.footer?.html ?? "";
279
+ const header = office ? "" : settings.header?.html ?? "";
280
+ const footer = office ? "" : settings.footer?.html ?? "";
231
281
  if (settings.header) settings.header = stripHtml(settings.header);
232
282
  if (settings.footer) settings.footer = stripHtml(settings.footer);
233
283
  (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "template.json"), JSON.stringify(meta, null, 2) + "\n");
234
- (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "template.html"), version.html ?? "");
235
- writeOrRemove((0, import_node_path2.join)(dir, "style.css"), version.css ?? "");
236
- 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 ?? "");
237
290
  writeOrRemove((0, import_node_path2.join)(dir, "header.html"), header);
238
291
  writeOrRemove((0, import_node_path2.join)(dir, "footer.html"), footer);
239
292
  (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
@@ -268,25 +321,38 @@ function writeOrRemove(path, content) {
268
321
  if (content) (0, import_node_fs2.writeFileSync)(path, content);
269
322
  else if ((0, import_node_fs2.existsSync)(path)) (0, import_node_fs2.rmSync)(path);
270
323
  }
271
- function versionPayload(tpl) {
324
+ function commonPayload(tpl) {
272
325
  const sample = tpl.dataSets["default"] ?? Object.values(tpl.dataSets)[0] ?? {};
273
326
  const named = Object.fromEntries(
274
327
  Object.entries(tpl.dataSets).filter(([name]) => name !== "default")
275
328
  );
276
329
  return {
277
- html: tpl.html,
278
- css: tpl.css,
279
- head: tpl.head,
280
330
  settings: tpl.settings,
281
331
  sample_data: sample ?? {},
282
332
  data_sets: named,
283
333
  data_schema: tpl.dataSchema,
284
- i18n: tpl.i18n,
334
+ i18n: tpl.i18n
335
+ };
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),
285
346
  // only when the template includes one, so a project without partials sends what it always sent
286
347
  ...Object.keys(tpl.partials).length ? { partials: tpl.partials } : {}
287
348
  };
288
349
  }
289
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
+ }
290
356
  const payload = versionPayload(tpl);
291
357
  const parts = [payload.html, payload.css, payload.head, payload.settings, payload.sample_data, payload.data_sets, payload.data_schema, payload.i18n];
292
358
  if (payload.partials) parts.push(payload.partials);
@@ -307,6 +373,7 @@ function recordSync(project, slug, version, tpl) {
307
373
  number: version.number,
308
374
  status: version.status,
309
375
  contentHash: contentHash(tpl),
376
+ ...tpl.file ? { fileSha256: tpl.file.sha256 } : {},
310
377
  syncedAt: (/* @__PURE__ */ new Date()).toISOString()
311
378
  };
312
379
  writeState(project, state);
@@ -315,24 +382,85 @@ function titleFromSlug(slug) {
315
382
  return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
316
383
  }
317
384
  function partialResolver(project) {
385
+ let shared = null;
318
386
  return (name) => {
319
387
  for (const candidate of [name, `${name}.html`]) {
320
388
  const path = (0, import_node_path2.join)(project.partialsDir, candidate);
321
389
  if ((0, import_node_fs2.existsSync)(path) && (0, import_node_fs2.statSync)(path).isFile()) return (0, import_node_fs2.readFileSync)(path, "utf8");
322
390
  }
323
- 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"])
324
436
  };
325
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
+ }
326
453
 
327
454
  // packages/devkit/src/lib/local-render.ts
328
- var import_engine2 = require("@formfeed/engine");
455
+ var import_engine4 = require("@formfeed/engine");
329
456
  function diagnose(tpl, sampleData) {
330
- const engine = (0, import_engine2.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);
331
459
  const diagnostics = [];
332
460
  try {
333
461
  engine.compile(tpl.html, { name: tpl.slug });
334
462
  } catch (e) {
335
- if (e instanceof import_engine2.EngineSyntaxError) {
463
+ if (e instanceof import_engine4.EngineSyntaxError) {
336
464
  diagnostics.push({
337
465
  severity: "error",
338
466
  code: "syntax-error",
@@ -358,45 +486,105 @@ function diagnose(tpl, sampleData) {
358
486
  });
359
487
  return diagnostics;
360
488
  }
361
- function renderContext(project, tpl, assetBaseUrl) {
362
- const settings = (0, import_engine2.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);
363
543
  return {
364
544
  locale: settings.locale ?? "en",
365
545
  timezone: settings.timezone ?? "UTC",
366
546
  currency: settings.currency ?? "EUR",
367
547
  partials: partialResolver(project),
368
- helpers: (0, import_engine2.defaultHelpers)(),
369
- limits: import_engine2.defaultLimits,
548
+ helpers: (0, import_engine4.defaultHelpers)(),
549
+ limits: import_engine4.defaultLimits,
370
550
  i18n: tpl.i18n ?? void 0,
371
- assetBaseUrl
551
+ assetBaseUrl,
552
+ // the organisation's kit from `formfeed brand pull`, else the empty kit the server uses too
553
+ brand: brand ?? readBrand(project)
372
554
  };
373
555
  }
374
556
  async function renderLocal(project, tpl, data, options) {
375
- const ctx = renderContext(project, tpl, options.assetBaseUrl);
557
+ const kind = htmlKind(tpl);
558
+ const ctx = renderContext(project, tpl, options.assetBaseUrl, options.brand);
376
559
  if (options.locale) ctx.locale = options.locale;
377
- return (0, import_engine2.renderVersion)(
378
- { engine: tpl.meta.engine, html: tpl.html, css: tpl.css, head: tpl.head, settings: (0, import_engine2.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 },
379
562
  data,
380
563
  ctx,
381
564
  options.mode,
382
565
  options.vendor ? { vendor: options.vendor } : {}
383
566
  );
384
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
+ }
385
572
  function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
573
+ const kind = htmlKind(tpl);
386
574
  const draft = {
387
575
  document: rendered.document,
388
576
  headerHtml: rendered.headerHtml,
389
577
  footerHtml: rendered.footerHtml,
390
578
  settings: rendered.settings,
391
- kind: tpl.meta.kind
579
+ kind
392
580
  };
393
- return mode === "paged" && tpl.meta.kind === "pdf" ? (0, import_engine2.pagedDocument)(draft, { pagedScriptUrl }) : (0, import_engine2.flowDocument)(draft);
581
+ return mode === "paged" && kind === "pdf" ? (0, import_engine4.pagedDocument)(draft, { pagedScriptUrl }) : (0, import_engine4.flowDocument)(draft);
394
582
  }
395
583
 
396
584
  // packages/devkit/src/lib/files.ts
397
585
  var import_node_crypto2 = require("node:crypto");
398
- var import_node_fs3 = require("node:fs");
399
- var import_node_path3 = require("node:path");
586
+ var import_node_fs4 = require("node:fs");
587
+ var import_node_path4 = require("node:path");
400
588
 
401
589
  // packages/api-types/src/lib/assets.ts
402
590
  var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
@@ -428,70 +616,644 @@ function sha256File(bytes) {
428
616
  }
429
617
  function listLocalFiles(project) {
430
618
  const listing = { files: [], skipped: [] };
431
- if (!(0, import_node_fs3.existsSync)(project.filesDir)) return listing;
432
- const walk = (dir) => {
433
- for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
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 })) {
434
622
  if (entry.name.startsWith(".")) continue;
435
- const path = (0, import_node_path3.join)(dir, entry.name);
623
+ const path = (0, import_node_path4.join)(dir, entry.name);
436
624
  if (entry.isDirectory()) {
437
- walk(path);
625
+ walk2(path);
438
626
  continue;
439
627
  }
440
628
  if (!entry.isFile()) continue;
441
- const name = (0, import_node_path3.relative)(project.filesDir, path).split(import_node_path3.sep).join("/");
442
- const size = (0, import_node_fs3.statSync)(path).size;
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;
443
631
  const contentType = contentTypeFor(name);
444
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;
445
633
  if (reason) {
446
634
  listing.skipped.push({ path, reason });
447
635
  continue;
448
636
  }
449
- const bytes = (0, import_node_fs3.readFileSync)(path);
637
+ const bytes = (0, import_node_fs4.readFileSync)(path);
450
638
  listing.files.push({ name, path, bytes: size, sha256: sha256File(bytes), contentType });
451
639
  }
452
640
  };
453
- walk(project.filesDir);
641
+ walk2(project.filesDir);
454
642
  listing.files.sort((a, b) => a.name.localeCompare(b.name));
455
643
  return listing;
456
644
  }
457
645
  function localFilePath(project, name) {
458
646
  if (!isAssetName(name)) throw new DevkitError(`"${name}" is not a library file name`);
459
- const path = (0, import_node_path3.resolve)(project.filesDir, ...name.split("/"));
460
- if (!path.startsWith((0, import_node_path3.resolve)(project.filesDir) + import_node_path3.sep)) throw new DevkitError(`"${name}" is not inside the files folder`);
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`);
461
649
  return path;
462
650
  }
463
651
  function writeLocalFile(project, name, bytes) {
464
652
  const path = localFilePath(project, name);
465
- (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(path), { recursive: true });
466
- (0, import_node_fs3.writeFileSync)(path, bytes);
653
+ (0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
654
+ (0, import_node_fs4.writeFileSync)(path, bytes);
467
655
  return path;
468
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);
1211
+ }
469
1212
  // Annotate the CommonJS export names for ESM import in node:
470
1213
  0 && (module.exports = {
471
1214
  DevkitError,
1215
+ brandFromJson,
1216
+ brandPath,
472
1217
  collectPartials,
473
1218
  contentHash,
474
1219
  contentTypeFor,
475
1220
  defaultData,
476
1221
  defaultProjectConfig,
1222
+ defaultTypesFile,
477
1223
  diagnose,
478
1224
  findProject,
1225
+ generateTypes,
479
1226
  listLocalFiles,
480
1227
  listTemplateSlugs,
481
1228
  loadProject,
482
1229
  localFilePath,
1230
+ localTypeSource,
1231
+ officeSnapshot,
1232
+ officeVersionPayload,
1233
+ parseRedactPath,
1234
+ partialContentHash,
483
1235
  partialResolver,
1236
+ prettyXml,
484
1237
  previewDocument,
485
1238
  projectAround,
1239
+ readBrand,
486
1240
  readState,
487
1241
  readTemplate,
1242
+ recordSharedPartial,
488
1243
  recordSync,
1244
+ redactData,
1245
+ redactString,
489
1246
  renderContext,
490
1247
  renderLocal,
1248
+ renderOfficeLocal,
491
1249
  sha256File,
1250
+ sharedPartialNames,
1251
+ sharedPartialPath,
492
1252
  templateDir,
1253
+ templateFileName,
493
1254
  titleFromSlug,
494
1255
  versionPayload,
1256
+ writeBrand,
495
1257
  writeLocalFile,
496
1258
  writePartials,
497
1259
  writeProjectConfig,