@formfeed/devkit 0.2.0 → 0.3.1

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