@formfeed/devkit 0.3.10 → 0.3.12

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
@@ -26,10 +26,12 @@ __export(index_exports, {
26
26
  collectPartials: () => collectPartials,
27
27
  contentHash: () => contentHash,
28
28
  contentTypeFor: () => contentTypeFor,
29
+ declaredEinvoice: () => declaredEinvoice,
29
30
  defaultData: () => defaultData,
30
31
  defaultProjectConfig: () => defaultProjectConfig,
31
32
  defaultTypesFile: () => defaultTypesFile,
32
33
  diagnose: () => diagnose,
34
+ diagnoseEinvoice: () => diagnoseEinvoice,
33
35
  findProject: () => findProject,
34
36
  generateTypes: () => generateTypes,
35
37
  htmlSnapshot: () => htmlSnapshot,
@@ -37,6 +39,7 @@ __export(index_exports, {
37
39
  listLocalFiles: () => listLocalFiles,
38
40
  listTemplateSlugs: () => listTemplateSlugs,
39
41
  loadProject: () => loadProject,
42
+ localEinvoiceXml: () => localEinvoiceXml,
40
43
  localFilePath: () => localFilePath,
41
44
  localTypeSource: () => localTypeSource,
42
45
  officeSnapshot: () => officeSnapshot,
@@ -221,16 +224,23 @@ function readJson(path, what) {
221
224
  try {
222
225
  return JSON.parse(text2);
223
226
  } catch (e) {
224
- throw new DevkitError(`${what} is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`, "validation");
227
+ throw new DevkitError(
228
+ `${what} is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`,
229
+ "validation"
230
+ );
225
231
  }
226
232
  }
227
233
  var templateFileName = (kind) => (0, import_engine2.isOfficeKind)(kind) ? `template.${kind}` : "template.html";
228
234
  function sourceFilesIn(dir) {
229
- return ["template.html", ...import_engine2.officeKinds.map((k) => `template.${k}`)].filter((name) => (0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, name)));
235
+ return ["template.html", ...import_engine2.officeKinds.map((k) => `template.${k}`)].filter(
236
+ (name) => (0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, name))
237
+ );
230
238
  }
231
239
  function listTemplateSlugs(project) {
232
240
  if (!(0, import_node_fs2.existsSync)(project.templatesDir)) return [];
233
- return (0, import_node_fs2.readdirSync)(project.templatesDir).filter((name) => sourceFilesIn((0, import_node_path2.join)(project.templatesDir, name)).length > 0).sort();
241
+ return (0, import_node_fs2.readdirSync)(project.templatesDir).filter(
242
+ (name) => sourceFilesIn((0, import_node_path2.join)(project.templatesDir, name)).length > 0
243
+ ).sort();
234
244
  }
235
245
  function templateDir(project, slug) {
236
246
  return (0, import_node_path2.join)(project.templatesDir, slug);
@@ -239,22 +249,41 @@ var sha256Of = (bytes) => (0, import_node_crypto.createHash)("sha256").update(by
239
249
  function readOfficeFile(dir, name) {
240
250
  const path = (0, import_node_path2.join)(dir, name);
241
251
  const bytes = new Uint8Array((0, import_node_fs2.readFileSync)(path));
242
- return { format: (0, import_node_path2.extname)(name).slice(1), path, bytes, sha256: sha256Of(bytes) };
252
+ return {
253
+ format: (0, import_node_path2.extname)(name).slice(1),
254
+ path,
255
+ bytes,
256
+ sha256: sha256Of(bytes)
257
+ };
243
258
  }
244
259
  function readTemplate(project, slug) {
245
260
  const dir = templateDir(project, slug);
246
261
  const sources = sourceFilesIn(dir);
247
262
  if (sources.length === 0)
248
- throw new DevkitError(`No template "${slug}" in ${project.templatesDir} (template.html, template.docx or template.pptx missing)`);
263
+ throw new DevkitError(
264
+ `No template "${slug}" in ${project.templatesDir} (template.html, template.docx or template.pptx missing)`
265
+ );
249
266
  if (sources.length > 1)
250
- throw new DevkitError(`${dir} holds ${sources.join(" and ")}; a template folder has one of them`, "validation");
267
+ throw new DevkitError(
268
+ `${dir} holds ${sources.join(" and ")}; a template folder has one of them`,
269
+ "validation"
270
+ );
251
271
  const file = sources[0] === "template.html" ? null : readOfficeFile(dir, sources[0]);
252
272
  const html = file ? "" : readText((0, import_node_path2.join)(dir, "template.html")) ?? "";
253
- const metaFile = readJson((0, import_node_path2.join)(dir, "template.json"), "template.json") ?? {};
273
+ const metaFile = readJson(
274
+ (0, import_node_path2.join)(dir, "template.json"),
275
+ "template.json"
276
+ ) ?? {};
254
277
  if (file && metaFile.kind && metaFile.kind !== file.format)
255
- throw new DevkitError(`${slug}: template.json says kind "${metaFile.kind}", but the folder holds ${(0, import_node_path2.basename)(file.path)}`, "validation");
278
+ throw new DevkitError(
279
+ `${slug}: template.json says kind "${metaFile.kind}", but the folder holds ${(0, import_node_path2.basename)(file.path)}`,
280
+ "validation"
281
+ );
256
282
  if (!file && (0, import_engine2.isOfficeKind)(metaFile.kind))
257
- throw new DevkitError(`${slug}: template.json says kind "${metaFile.kind}", but the folder holds template.html instead of template.${metaFile.kind}`, "validation");
283
+ throw new DevkitError(
284
+ `${slug}: template.json says kind "${metaFile.kind}", but the folder holds template.html instead of template.${metaFile.kind}`,
285
+ "validation"
286
+ );
258
287
  const meta = {
259
288
  name: metaFile.name ?? titleFromSlug(slug),
260
289
  kind: file?.format ?? metaFile.kind ?? "pdf",
@@ -262,19 +291,38 @@ function readTemplate(project, slug) {
262
291
  description: metaFile.description ?? null,
263
292
  tags: metaFile.tags ?? []
264
293
  };
265
- const settings = { ...readJson((0, import_node_path2.join)(dir, "settings.json"), "settings.json") ?? {} };
294
+ const settings = {
295
+ ...readJson(
296
+ (0, import_node_path2.join)(dir, "settings.json"),
297
+ "settings.json"
298
+ ) ?? {}
299
+ };
266
300
  const header = file ? null : readText((0, import_node_path2.join)(dir, "header.html"));
267
301
  const footer = file ? null : readText((0, import_node_path2.join)(dir, "footer.html"));
268
- if (header !== null && header.trim()) settings.header = { ...settings.header ?? {}, html: header };
269
- if (footer !== null && footer.trim()) settings.footer = { ...settings.footer ?? {}, html: footer };
302
+ if (header !== null && header.trim())
303
+ settings.header = { ...settings.header ?? {}, html: header };
304
+ if (footer !== null && footer.trim())
305
+ settings.footer = { ...settings.footer ?? {}, html: footer };
306
+ const design = readJson(
307
+ (0, import_node_path2.join)(dir, "design.json"),
308
+ "design.json"
309
+ );
310
+ if (design) settings.design = design;
270
311
  const dataSets = {};
271
312
  const dataDir = (0, import_node_path2.join)(dir, "data");
272
313
  if ((0, import_node_fs2.existsSync)(dataDir)) {
273
314
  for (const file2 of (0, import_node_fs2.readdirSync)(dataDir).filter((f) => f.endsWith(".json")).sort()) {
274
- dataSets[(0, import_node_path2.basename)(file2, ".json")] = readJson((0, import_node_path2.join)(dataDir, file2), `data/${file2}`);
315
+ dataSets[(0, import_node_path2.basename)(file2, ".json")] = readJson(
316
+ (0, import_node_path2.join)(dataDir, file2),
317
+ `data/${file2}`
318
+ );
275
319
  }
276
320
  }
277
- const { partials, missing, shared } = file ? { partials: {}, missing: [], shared: [] } : collectPartials(project, meta.engine, [html, settings.header?.html, settings.footer?.html]);
321
+ const { partials, missing, shared } = file ? { partials: {}, missing: [], shared: [] } : collectPartials(project, meta.engine, [
322
+ html,
323
+ settings.header?.html,
324
+ settings.footer?.html
325
+ ]);
278
326
  return {
279
327
  slug,
280
328
  dir,
@@ -285,8 +333,14 @@ function readTemplate(project, slug) {
285
333
  head: file ? "" : readText((0, import_node_path2.join)(dir, "head.html")) ?? "",
286
334
  settings,
287
335
  dataSets,
288
- dataSchema: readJson((0, import_node_path2.join)(dir, "schema.json"), "schema.json"),
289
- i18n: readJson((0, import_node_path2.join)(dir, "i18n.json"), "i18n.json"),
336
+ dataSchema: readJson(
337
+ (0, import_node_path2.join)(dir, "schema.json"),
338
+ "schema.json"
339
+ ),
340
+ i18n: readJson(
341
+ (0, import_node_path2.join)(dir, "i18n.json"),
342
+ "i18n.json"
343
+ ),
290
344
  partials,
291
345
  missingPartials: missing,
292
346
  sharedPartials: shared
@@ -296,35 +350,68 @@ function defaultData(tpl, name) {
296
350
  const names = Object.keys(tpl.dataSets);
297
351
  const chosen = name ?? (names.includes("default") ? "default" : names[0]);
298
352
  if (name && !(name in tpl.dataSets))
299
- throw new DevkitError(`Data set "${name}" not found; available: ${names.join(", ") || "none"}`);
353
+ throw new DevkitError(
354
+ `Data set "${name}" not found; available: ${names.join(", ") || "none"}`
355
+ );
300
356
  return chosen ? { name: chosen, data: tpl.dataSets[chosen] } : { name: "empty", data: {} };
301
357
  }
302
358
  function writeTemplate(project, slug, meta, version, file) {
303
359
  const dir = templateDir(project, slug);
304
360
  const office = (0, import_engine2.isOfficeKind)(meta.kind);
305
- if (office && !file) throw new DevkitError(`${slug} is a ${meta.kind} template; its document is needed to write the folder`);
361
+ if (office && !file)
362
+ throw new DevkitError(
363
+ `${slug} is a ${meta.kind} template; its document is needed to write the folder`
364
+ );
306
365
  (0, import_node_fs2.mkdirSync)((0, import_node_path2.join)(dir, "data"), { recursive: true });
307
366
  const settings = { ...version.settings ?? {} };
308
367
  const header = office ? "" : settings.header?.html ?? "";
309
368
  const footer = office ? "" : settings.footer?.html ?? "";
310
369
  if (settings.header) settings.header = stripHtml(settings.header);
311
370
  if (settings.footer) settings.footer = stripHtml(settings.footer);
312
- (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "template.json"), JSON.stringify(meta, null, 2) + "\n");
313
- for (const name of ["template.html", ...import_engine2.officeKinds.map((k) => `template.${k}`)])
314
- 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));
371
+ const design = settings.design;
372
+ delete settings.design;
373
+ (0, import_node_fs2.writeFileSync)(
374
+ (0, import_node_path2.join)(dir, "template.json"),
375
+ JSON.stringify(meta, null, 2) + "\n"
376
+ );
377
+ for (const name of [
378
+ "template.html",
379
+ ...import_engine2.officeKinds.map((k) => `template.${k}`)
380
+ ])
381
+ if (name !== templateFileName(meta.kind) && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(dir, name)))
382
+ (0, import_node_fs2.rmSync)((0, import_node_path2.join)(dir, name));
315
383
  if (office) (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, templateFileName(meta.kind)), file);
316
384
  else (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "template.html"), version.html ?? "");
317
385
  writeOrRemove((0, import_node_path2.join)(dir, "style.css"), office ? "" : version.css ?? "");
318
386
  writeOrRemove((0, import_node_path2.join)(dir, "head.html"), office ? "" : version.head ?? "");
319
387
  writeOrRemove((0, import_node_path2.join)(dir, "header.html"), header);
320
388
  writeOrRemove((0, import_node_path2.join)(dir, "footer.html"), footer);
321
- (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
322
- (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "data", "default.json"), JSON.stringify(version.sample_data ?? {}, null, 2) + "\n");
389
+ (0, import_node_fs2.writeFileSync)(
390
+ (0, import_node_path2.join)(dir, "settings.json"),
391
+ JSON.stringify(settings, null, 2) + "\n"
392
+ );
393
+ writeOrRemove(
394
+ (0, import_node_path2.join)(dir, "design.json"),
395
+ design && typeof design === "object" ? JSON.stringify(design, null, 2) + "\n" : ""
396
+ );
397
+ (0, import_node_fs2.writeFileSync)(
398
+ (0, import_node_path2.join)(dir, "data", "default.json"),
399
+ JSON.stringify(version.sample_data ?? {}, null, 2) + "\n"
400
+ );
323
401
  for (const [name, data] of Object.entries(version.data_sets ?? {}))
324
402
  if (name !== "default" && /^[\w.-]+$/.test(name))
325
- (0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "data", `${name}.json`), JSON.stringify(data ?? {}, null, 2) + "\n");
326
- writeOrRemove((0, import_node_path2.join)(dir, "schema.json"), version.data_schema ? JSON.stringify(version.data_schema, null, 2) + "\n" : "");
327
- writeOrRemove((0, import_node_path2.join)(dir, "i18n.json"), version.i18n ? JSON.stringify(version.i18n, null, 2) + "\n" : "");
403
+ (0, import_node_fs2.writeFileSync)(
404
+ (0, import_node_path2.join)(dir, "data", `${name}.json`),
405
+ JSON.stringify(data ?? {}, null, 2) + "\n"
406
+ );
407
+ writeOrRemove(
408
+ (0, import_node_path2.join)(dir, "schema.json"),
409
+ version.data_schema ? JSON.stringify(version.data_schema, null, 2) + "\n" : ""
410
+ );
411
+ writeOrRemove(
412
+ (0, import_node_path2.join)(dir, "i18n.json"),
413
+ version.i18n ? JSON.stringify(version.i18n, null, 2) + "\n" : ""
414
+ );
328
415
  writePartials(project, version.partials ?? null);
329
416
  return dir;
330
417
  }
@@ -333,7 +420,10 @@ function writePartials(project, partials) {
333
420
  const changed = [];
334
421
  for (const [name, source] of Object.entries(partials ?? {})) {
335
422
  if (!partialName.test(name)) continue;
336
- const path = (0, import_node_path2.join)(project.partialsDir, name.includes(".") ? name : `${name}.html`);
423
+ const path = (0, import_node_path2.join)(
424
+ project.partialsDir,
425
+ name.includes(".") ? name : `${name}.html`
426
+ );
337
427
  if (readText(path) === source) continue;
338
428
  (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
339
429
  (0, import_node_fs2.writeFileSync)(path, source);
@@ -379,17 +469,36 @@ function versionPayload(tpl) {
379
469
  function contentHash(tpl) {
380
470
  if (tpl.file) {
381
471
  const payload2 = officeVersionPayload(tpl);
382
- const parts2 = [tpl.file.format, tpl.file.sha256, payload2.settings, payload2.sample_data, payload2.data_sets, payload2.data_schema, payload2.i18n];
472
+ const parts2 = [
473
+ tpl.file.format,
474
+ tpl.file.sha256,
475
+ payload2.settings,
476
+ payload2.sample_data,
477
+ payload2.data_sets,
478
+ payload2.data_schema,
479
+ payload2.i18n
480
+ ];
383
481
  return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(parts2)).digest("hex");
384
482
  }
385
483
  const payload = versionPayload(tpl);
386
- const parts = [payload.html, payload.css, payload.head, payload.settings, payload.sample_data, payload.data_sets, payload.data_schema, payload.i18n];
484
+ const parts = [
485
+ payload.html,
486
+ payload.css,
487
+ payload.head,
488
+ payload.settings,
489
+ payload.sample_data,
490
+ payload.data_sets,
491
+ payload.data_schema,
492
+ payload.i18n
493
+ ];
387
494
  if (payload.partials) parts.push(payload.partials);
388
495
  return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(parts)).digest("hex");
389
496
  }
390
497
  var statePath = (project) => (0, import_node_path2.join)(project.root, ".formfeed", "state.json");
391
498
  function readState(project) {
392
- return readJson(statePath(project), ".formfeed/state.json") ?? { templates: {} };
499
+ return readJson(statePath(project), ".formfeed/state.json") ?? {
500
+ templates: {}
501
+ };
393
502
  }
394
503
  function writeState(project, state) {
395
504
  (0, import_node_fs2.mkdirSync)((0, import_node_path2.join)(project.root, ".formfeed"), { recursive: true });
@@ -415,7 +524,8 @@ function partialResolver(project) {
415
524
  return (name) => {
416
525
  for (const candidate of [name, `${name}.html`]) {
417
526
  const path = (0, import_node_path2.join)(project.partialsDir, candidate);
418
- if ((0, import_node_fs2.existsSync)(path) && (0, import_node_fs2.statSync)(path).isFile()) return (0, import_node_fs2.readFileSync)(path, "utf8");
527
+ if ((0, import_node_fs2.existsSync)(path) && (0, import_node_fs2.statSync)(path).isFile())
528
+ return (0, import_node_fs2.readFileSync)(path, "utf8");
419
529
  }
420
530
  shared ??= new Set(Object.keys(readState(project).sharedPartials ?? {}));
421
531
  const normalised = (0, import_engine2.normalisePartialName)(name);
@@ -436,7 +546,12 @@ function recordSharedPartial(project, name, remote, source) {
436
546
  const state = readState(project);
437
547
  state.sharedPartials = {
438
548
  ...state.sharedPartials ?? {},
439
- [name]: { version: remote.version, engine: remote.engine, contentHash: partialContentHash(source), syncedAt: (/* @__PURE__ */ new Date()).toISOString() }
549
+ [name]: {
550
+ version: remote.version,
551
+ engine: remote.engine,
552
+ contentHash: partialContentHash(source),
553
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString()
554
+ }
440
555
  };
441
556
  writeState(project, state);
442
557
  }
@@ -494,7 +609,10 @@ function diagnose(tpl, sampleData) {
494
609
  severity: "error",
495
610
  code: "syntax-error",
496
611
  message: e.message,
497
- range: { start: { line: e.line ?? 1, column: e.column ?? 1 }, end: { line: e.line ?? 1, column: (e.column ?? 1) + 1 } }
612
+ range: {
613
+ start: { line: e.line ?? 1, column: e.column ?? 1 },
614
+ end: { line: e.line ?? 1, column: (e.column ?? 1) + 1 }
615
+ }
498
616
  });
499
617
  } else throw e;
500
618
  }
@@ -504,7 +622,8 @@ function diagnose(tpl, sampleData) {
504
622
  ["footer", tpl.settings.footer?.html]
505
623
  ]) {
506
624
  if (!source) continue;
507
- for (const d of engine.analyze(source, { sampleData }).diagnostics) diagnostics.push({ ...d, message: `${part}: ${d.message}` });
625
+ for (const d of engine.analyze(source, { sampleData }).diagnostics)
626
+ diagnostics.push({ ...d, message: `${part}: ${d.message}` });
508
627
  }
509
628
  for (const name of tpl.missingPartials)
510
629
  diagnostics.push({
@@ -514,8 +633,28 @@ function diagnose(tpl, sampleData) {
514
633
  message: `No partial "${name}" in the project's partials folder; if it is shared by the organisation, formfeed partials pull brings it`,
515
634
  range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }
516
635
  });
636
+ const design = designDiagnostic(tpl);
637
+ if (design) diagnostics.push(design);
517
638
  return diagnostics;
518
639
  }
640
+ function designDiagnostic(tpl) {
641
+ if (tpl.settings.design === void 0) return null;
642
+ const parsed = (0, import_engine4.parseDesign)(tpl.settings.design);
643
+ if (!parsed.design)
644
+ return {
645
+ severity: "warning",
646
+ code: "design-invalid",
647
+ message: `design.json is not a design the app can open (${parsed.problems.slice(0, 3).join("; ")}); the template opens as code`,
648
+ range: nowhere
649
+ };
650
+ if (parsed.design.output === (0, import_engine4.designOutputHash)(tpl.html, tpl.css)) return null;
651
+ return {
652
+ severity: "warning",
653
+ code: "design-stale",
654
+ message: "template.html or style.css was edited after design.json was written; the app opens the template as code until you choose to keep the code or apply the design again",
655
+ range: nowhere
656
+ };
657
+ }
519
658
  var nowhere = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };
520
659
  function diagnoseOffice(tpl, file, sampleData) {
521
660
  let analysis;
@@ -523,7 +662,14 @@ function diagnoseOffice(tpl, file, sampleData) {
523
662
  analysis = (0, import_engine4.analyzeOffice)(file, { engine: tpl.meta.engine, sampleData });
524
663
  } catch (e) {
525
664
  const code = e.code;
526
- return [{ severity: "error", code: typeof code === "string" ? code : "office-document-invalid", message: e instanceof Error ? e.message : String(e), range: nowhere }];
665
+ return [
666
+ {
667
+ severity: "error",
668
+ code: typeof code === "string" ? code : "office-document-invalid",
669
+ message: e instanceof Error ? e.message : String(e),
670
+ range: nowhere
671
+ }
672
+ ];
527
673
  }
528
674
  return analysis.diagnostics.map((d) => ({
529
675
  severity: d.severity,
@@ -535,17 +681,34 @@ function diagnoseOffice(tpl, file, sampleData) {
535
681
  }));
536
682
  }
537
683
  async function renderOfficeLocal(project, tpl, data, options = {}) {
538
- if (!tpl.file) throw new DevkitError(`${tpl.slug} is not a Word or PowerPoint template`, "validation");
539
- const context = renderContext(project, tpl, options.assetBaseUrl, options.brand);
684
+ if (!tpl.file)
685
+ throw new DevkitError(
686
+ `${tpl.slug} is not a Word or PowerPoint template`,
687
+ "validation"
688
+ );
689
+ const context = renderContext(
690
+ project,
691
+ tpl,
692
+ options.assetBaseUrl,
693
+ options.brand
694
+ );
540
695
  if (options.locale) context.locale = options.locale;
541
- return (0, import_engine4.renderOffice)(tpl.file.bytes, { engine: tpl.meta.engine, data, context, images: options.images, random: options.random });
696
+ return (0, import_engine4.renderOffice)(tpl.file.bytes, {
697
+ engine: tpl.meta.engine,
698
+ data,
699
+ context,
700
+ images: options.images,
701
+ random: options.random
702
+ });
542
703
  }
543
704
  function htmlSnapshot(rendered) {
544
705
  let snapshot = rendered.document;
545
- if (rendered.headerHtml) snapshot += `
706
+ if (rendered.headerHtml)
707
+ snapshot += `
546
708
  <!-- formfeed:header -->
547
709
  ${rendered.headerHtml}`;
548
- if (rendered.footerHtml) snapshot += `
710
+ if (rendered.footerHtml)
711
+ snapshot += `
549
712
  <!-- formfeed:footer -->
550
713
  ${rendered.footerHtml}`;
551
714
  return snapshot;
@@ -598,7 +761,14 @@ async function renderLocal(project, tpl, data, options) {
598
761
  const ctx = renderContext(project, tpl, options.assetBaseUrl, options.brand);
599
762
  if (options.locale) ctx.locale = options.locale;
600
763
  return (0, import_engine4.renderVersion)(
601
- { engine: tpl.meta.engine, html: tpl.html, css: tpl.css, head: tpl.head, settings: (0, import_engine4.mergeSettings)(tpl.settings), kind },
764
+ {
765
+ engine: tpl.meta.engine,
766
+ html: tpl.html,
767
+ css: tpl.css,
768
+ head: tpl.head,
769
+ settings: (0, import_engine4.mergeSettings)(tpl.settings),
770
+ kind
771
+ },
602
772
  data,
603
773
  ctx,
604
774
  options.mode,
@@ -608,16 +778,26 @@ async function renderLocal(project, tpl, data, options) {
608
778
  function renderedSettings(rendered) {
609
779
  const style = rendered.inlineCss ? `<style>${rendered.inlineCss}</style>` : "";
610
780
  const part = (settings2, html) => settings2?.html ? { ...settings2, html: `${style}${html ?? ""}` } : settings2;
611
- const { settings } = rendered;
781
+ const { design: _design, ...settings } = rendered.settings;
782
+ void _design;
612
783
  return {
613
784
  ...settings,
614
785
  ...settings.header ? { header: part(settings.header, rendered.headerHtml) } : {},
615
786
  ...settings.footer ? { footer: part(settings.footer, rendered.footerHtml) } : {},
616
- ...settings.pdf?.metadata?.title !== void 0 ? { pdf: { ...settings.pdf, metadata: { ...settings.pdf.metadata, title: rendered.title ?? "" } } } : {}
787
+ ...settings.pdf?.metadata?.title !== void 0 ? {
788
+ pdf: {
789
+ ...settings.pdf,
790
+ metadata: { ...settings.pdf.metadata, title: rendered.title ?? "" }
791
+ }
792
+ } : {}
617
793
  };
618
794
  }
619
795
  function htmlKind(tpl) {
620
- if ((0, import_engine4.isOfficeKind)(tpl.meta.kind)) throw new DevkitError(`${tpl.slug} is a ${tpl.meta.kind} template; fill it with renderOfficeLocal`, "validation");
796
+ if ((0, import_engine4.isOfficeKind)(tpl.meta.kind))
797
+ throw new DevkitError(
798
+ `${tpl.slug} is a ${tpl.meta.kind} template; fill it with renderOfficeLocal`,
799
+ "validation"
800
+ );
621
801
  return tpl.meta.kind;
622
802
  }
623
803
  function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
@@ -632,10 +812,103 @@ function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
632
812
  return mode === "paged" && kind === "pdf" ? (0, import_engine4.pagedDocument)(draft, { pagedScriptUrl }) : (0, import_engine4.flowDocument)(draft);
633
813
  }
634
814
 
635
- // packages/devkit/src/lib/files.ts
636
- var import_node_crypto2 = require("node:crypto");
815
+ // packages/devkit/src/lib/einvoice.ts
637
816
  var import_node_fs4 = require("node:fs");
638
817
  var import_node_path4 = require("node:path");
818
+ var import_engine5 = require("@formfeed/engine");
819
+ function declaredEinvoice(tpl, override) {
820
+ const resolved = (0, import_engine5.resolveEinvoiceOptions)(tpl.settings.einvoice, override);
821
+ if (!resolved.ok)
822
+ throw new DevkitError(
823
+ `${tpl.slug}: e-invoice ${resolved.field} "${resolved.value}": ${resolved.reason}`,
824
+ "validation"
825
+ );
826
+ return resolved.options;
827
+ }
828
+ var nowhere2 = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };
829
+ function diagnoseEinvoice(tpl) {
830
+ let options;
831
+ try {
832
+ options = declaredEinvoice(tpl);
833
+ } catch (e) {
834
+ return [
835
+ {
836
+ dataSet: "",
837
+ file: "settings.json",
838
+ diagnostics: [
839
+ {
840
+ severity: "error",
841
+ code: "einvoice-profile",
842
+ message: e instanceof Error ? e.message : String(e),
843
+ range: nowhere2
844
+ }
845
+ ]
846
+ }
847
+ ];
848
+ }
849
+ if (!options) return [];
850
+ if (tpl.meta.kind === "image")
851
+ return [
852
+ {
853
+ dataSet: "",
854
+ file: "settings.json",
855
+ diagnostics: [
856
+ {
857
+ severity: "warning",
858
+ code: "einvoice-image",
859
+ message: "settings.json declares an e-invoice, which an image template never produces: an e-invoice is a PDF",
860
+ range: nowhere2
861
+ }
862
+ ]
863
+ }
864
+ ];
865
+ const out = [];
866
+ for (const [name, data] of Object.entries(tpl.dataSets)) {
867
+ const result = (0, import_engine5.einvoiceXml)(data, options);
868
+ if (result.ok) continue;
869
+ const file = `data/${name}.json`;
870
+ const path = (0, import_node_path4.join)(tpl.dir, "data", `${name}.json`);
871
+ const text2 = (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : null;
872
+ out.push({
873
+ dataSet: name,
874
+ file,
875
+ diagnostics: result.problems.map((problem) => {
876
+ const at = text2 ? (0, import_engine5.locateJsonPath)(text2, [
877
+ import_engine5.INVOICE_DATA_KEY,
878
+ ...(0, import_engine5.invoicePathSegments)(problem.path)
879
+ ]) : null;
880
+ const start = at ? { line: at.line, column: at.column } : nowhere2.start;
881
+ return {
882
+ severity: "error",
883
+ code: `einvoice-${problem.code.replace(/_/g, "-")}`,
884
+ message: problem.message,
885
+ range: { start, end: start }
886
+ };
887
+ })
888
+ });
889
+ }
890
+ return out;
891
+ }
892
+ function localEinvoiceXml(tpl, data, override) {
893
+ const options = declaredEinvoice(tpl, override ?? {});
894
+ if (!options)
895
+ throw new DevkitError(`${tpl.slug} declares no e-invoice`, "validation");
896
+ const result = (0, import_engine5.einvoiceXml)(data, options);
897
+ if (!result.ok)
898
+ throw new DevkitError(
899
+ [
900
+ `${tpl.slug}: the _invoice block does not make an e-invoice`,
901
+ ...result.problems.map((problem) => ` ${problem.message}`)
902
+ ].join("\n"),
903
+ "validation"
904
+ );
905
+ return { xml: result.xml, invoice: result.invoice, options };
906
+ }
907
+
908
+ // packages/devkit/src/lib/files.ts
909
+ var import_node_crypto2 = require("node:crypto");
910
+ var import_node_fs5 = require("node:fs");
911
+ var import_node_path5 = require("node:path");
639
912
 
640
913
  // packages/api-types/src/lib/assets.ts
641
914
  var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
@@ -667,25 +940,25 @@ function sha256File(bytes) {
667
940
  }
668
941
  function listLocalFiles(project) {
669
942
  const listing = { files: [], skipped: [] };
670
- if (!(0, import_node_fs4.existsSync)(project.filesDir)) return listing;
943
+ if (!(0, import_node_fs5.existsSync)(project.filesDir)) return listing;
671
944
  const walk2 = (dir) => {
672
- for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
945
+ for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
673
946
  if (entry.name.startsWith(".")) continue;
674
- const path = (0, import_node_path4.join)(dir, entry.name);
947
+ const path = (0, import_node_path5.join)(dir, entry.name);
675
948
  if (entry.isDirectory()) {
676
949
  walk2(path);
677
950
  continue;
678
951
  }
679
952
  if (!entry.isFile()) continue;
680
- const name = (0, import_node_path4.relative)(project.filesDir, path).split(import_node_path4.sep).join("/");
681
- const size = (0, import_node_fs4.statSync)(path).size;
953
+ const name = (0, import_node_path5.relative)(project.filesDir, path).split(import_node_path5.sep).join("/");
954
+ const size = (0, import_node_fs5.statSync)(path).size;
682
955
  const contentType = contentTypeFor(name);
683
956
  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;
684
957
  if (reason) {
685
958
  listing.skipped.push({ path, reason });
686
959
  continue;
687
960
  }
688
- const bytes = (0, import_node_fs4.readFileSync)(path);
961
+ const bytes = (0, import_node_fs5.readFileSync)(path);
689
962
  listing.files.push({ name, path, bytes: size, sha256: sha256File(bytes), contentType });
690
963
  }
691
964
  };
@@ -695,14 +968,14 @@ function listLocalFiles(project) {
695
968
  }
696
969
  function localFilePath(project, name) {
697
970
  if (!isAssetName(name)) throw new DevkitError(`"${name}" is not a library file name`);
698
- const path = (0, import_node_path4.resolve)(project.filesDir, ...name.split("/"));
699
- if (!path.startsWith((0, import_node_path4.resolve)(project.filesDir) + import_node_path4.sep)) throw new DevkitError(`"${name}" is not inside the files folder`);
971
+ const path = (0, import_node_path5.resolve)(project.filesDir, ...name.split("/"));
972
+ if (!path.startsWith((0, import_node_path5.resolve)(project.filesDir) + import_node_path5.sep)) throw new DevkitError(`"${name}" is not inside the files folder`);
700
973
  return path;
701
974
  }
702
975
  function writeLocalFile(project, name, bytes) {
703
976
  const path = localFilePath(project, name);
704
- (0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
705
- (0, import_node_fs4.writeFileSync)(path, bytes);
977
+ (0, import_node_fs5.mkdirSync)((0, import_node_path5.dirname)(path), { recursive: true });
978
+ (0, import_node_fs5.writeFileSync)(path, bytes);
706
979
  return path;
707
980
  }
708
981
 
@@ -770,7 +1043,7 @@ function isRecord(value) {
770
1043
  }
771
1044
 
772
1045
  // packages/devkit/src/lib/types/index.ts
773
- var import_engine5 = require("@formfeed/engine");
1046
+ var import_engine6 = require("@formfeed/engine");
774
1047
 
775
1048
  // packages/devkit/src/lib/types/model.ts
776
1049
  var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1258,7 +1531,7 @@ function localTypeSource(tpl) {
1258
1531
  const sets = Object.values(tpl.dataSets);
1259
1532
  return {
1260
1533
  slug: tpl.slug,
1261
- schema: (0, import_engine5.inferSchemaFromDataSets)(sets.length ? sets : [{}]),
1534
+ schema: (0, import_engine6.inferSchemaFromDataSets)(sets.length ? sets : [{}]),
1262
1535
  origin: "inferred"
1263
1536
  };
1264
1537
  }
@@ -1270,10 +1543,12 @@ function localTypeSource(tpl) {
1270
1543
  collectPartials,
1271
1544
  contentHash,
1272
1545
  contentTypeFor,
1546
+ declaredEinvoice,
1273
1547
  defaultData,
1274
1548
  defaultProjectConfig,
1275
1549
  defaultTypesFile,
1276
1550
  diagnose,
1551
+ diagnoseEinvoice,
1277
1552
  findProject,
1278
1553
  generateTypes,
1279
1554
  htmlSnapshot,
@@ -1281,6 +1556,7 @@ function localTypeSource(tpl) {
1281
1556
  listLocalFiles,
1282
1557
  listTemplateSlugs,
1283
1558
  loadProject,
1559
+ localEinvoiceXml,
1284
1560
  localFilePath,
1285
1561
  localTypeSource,
1286
1562
  officeSnapshot,