@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.js CHANGED
@@ -95,7 +95,15 @@ function writeProjectConfig(root, config) {
95
95
 
96
96
  // packages/devkit/src/lib/project.ts
97
97
  import { createHash } from "node:crypto";
98
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
98
+ import {
99
+ existsSync as existsSync2,
100
+ mkdirSync,
101
+ readFileSync as readFileSync2,
102
+ readdirSync,
103
+ rmSync,
104
+ statSync,
105
+ writeFileSync as writeFileSync2
106
+ } from "node:fs";
99
107
  import { basename, dirname as dirname2, extname, join as join2 } from "node:path";
100
108
  import {
101
109
  isOfficeKind,
@@ -149,16 +157,23 @@ function readJson(path, what) {
149
157
  try {
150
158
  return JSON.parse(text2);
151
159
  } catch (e) {
152
- throw new DevkitError(`${what} is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`, "validation");
160
+ throw new DevkitError(
161
+ `${what} is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`,
162
+ "validation"
163
+ );
153
164
  }
154
165
  }
155
166
  var templateFileName = (kind) => isOfficeKind(kind) ? `template.${kind}` : "template.html";
156
167
  function sourceFilesIn(dir) {
157
- return ["template.html", ...officeKinds.map((k) => `template.${k}`)].filter((name) => existsSync2(join2(dir, name)));
168
+ return ["template.html", ...officeKinds.map((k) => `template.${k}`)].filter(
169
+ (name) => existsSync2(join2(dir, name))
170
+ );
158
171
  }
159
172
  function listTemplateSlugs(project) {
160
173
  if (!existsSync2(project.templatesDir)) return [];
161
- return readdirSync(project.templatesDir).filter((name) => sourceFilesIn(join2(project.templatesDir, name)).length > 0).sort();
174
+ return readdirSync(project.templatesDir).filter(
175
+ (name) => sourceFilesIn(join2(project.templatesDir, name)).length > 0
176
+ ).sort();
162
177
  }
163
178
  function templateDir(project, slug) {
164
179
  return join2(project.templatesDir, slug);
@@ -167,22 +182,41 @@ var sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
167
182
  function readOfficeFile(dir, name) {
168
183
  const path = join2(dir, name);
169
184
  const bytes = new Uint8Array(readFileSync2(path));
170
- return { format: extname(name).slice(1), path, bytes, sha256: sha256Of(bytes) };
185
+ return {
186
+ format: extname(name).slice(1),
187
+ path,
188
+ bytes,
189
+ sha256: sha256Of(bytes)
190
+ };
171
191
  }
172
192
  function readTemplate(project, slug) {
173
193
  const dir = templateDir(project, slug);
174
194
  const sources = sourceFilesIn(dir);
175
195
  if (sources.length === 0)
176
- throw new DevkitError(`No template "${slug}" in ${project.templatesDir} (template.html, template.docx or template.pptx missing)`);
196
+ throw new DevkitError(
197
+ `No template "${slug}" in ${project.templatesDir} (template.html, template.docx or template.pptx missing)`
198
+ );
177
199
  if (sources.length > 1)
178
- throw new DevkitError(`${dir} holds ${sources.join(" and ")}; a template folder has one of them`, "validation");
200
+ throw new DevkitError(
201
+ `${dir} holds ${sources.join(" and ")}; a template folder has one of them`,
202
+ "validation"
203
+ );
179
204
  const file = sources[0] === "template.html" ? null : readOfficeFile(dir, sources[0]);
180
205
  const html = file ? "" : readText(join2(dir, "template.html")) ?? "";
181
- const metaFile = readJson(join2(dir, "template.json"), "template.json") ?? {};
206
+ const metaFile = readJson(
207
+ join2(dir, "template.json"),
208
+ "template.json"
209
+ ) ?? {};
182
210
  if (file && metaFile.kind && metaFile.kind !== file.format)
183
- throw new DevkitError(`${slug}: template.json says kind "${metaFile.kind}", but the folder holds ${basename(file.path)}`, "validation");
211
+ throw new DevkitError(
212
+ `${slug}: template.json says kind "${metaFile.kind}", but the folder holds ${basename(file.path)}`,
213
+ "validation"
214
+ );
184
215
  if (!file && isOfficeKind(metaFile.kind))
185
- throw new DevkitError(`${slug}: template.json says kind "${metaFile.kind}", but the folder holds template.html instead of template.${metaFile.kind}`, "validation");
216
+ throw new DevkitError(
217
+ `${slug}: template.json says kind "${metaFile.kind}", but the folder holds template.html instead of template.${metaFile.kind}`,
218
+ "validation"
219
+ );
186
220
  const meta = {
187
221
  name: metaFile.name ?? titleFromSlug(slug),
188
222
  kind: file?.format ?? metaFile.kind ?? "pdf",
@@ -190,19 +224,38 @@ function readTemplate(project, slug) {
190
224
  description: metaFile.description ?? null,
191
225
  tags: metaFile.tags ?? []
192
226
  };
193
- const settings = { ...readJson(join2(dir, "settings.json"), "settings.json") ?? {} };
227
+ const settings = {
228
+ ...readJson(
229
+ join2(dir, "settings.json"),
230
+ "settings.json"
231
+ ) ?? {}
232
+ };
194
233
  const header = file ? null : readText(join2(dir, "header.html"));
195
234
  const footer = file ? null : readText(join2(dir, "footer.html"));
196
- if (header !== null && header.trim()) settings.header = { ...settings.header ?? {}, html: header };
197
- if (footer !== null && footer.trim()) settings.footer = { ...settings.footer ?? {}, html: footer };
235
+ if (header !== null && header.trim())
236
+ settings.header = { ...settings.header ?? {}, html: header };
237
+ if (footer !== null && footer.trim())
238
+ settings.footer = { ...settings.footer ?? {}, html: footer };
239
+ const design = readJson(
240
+ join2(dir, "design.json"),
241
+ "design.json"
242
+ );
243
+ if (design) settings.design = design;
198
244
  const dataSets = {};
199
245
  const dataDir = join2(dir, "data");
200
246
  if (existsSync2(dataDir)) {
201
247
  for (const file2 of readdirSync(dataDir).filter((f) => f.endsWith(".json")).sort()) {
202
- dataSets[basename(file2, ".json")] = readJson(join2(dataDir, file2), `data/${file2}`);
248
+ dataSets[basename(file2, ".json")] = readJson(
249
+ join2(dataDir, file2),
250
+ `data/${file2}`
251
+ );
203
252
  }
204
253
  }
205
- const { partials, missing, shared } = file ? { partials: {}, missing: [], shared: [] } : collectPartials(project, meta.engine, [html, settings.header?.html, settings.footer?.html]);
254
+ const { partials, missing, shared } = file ? { partials: {}, missing: [], shared: [] } : collectPartials(project, meta.engine, [
255
+ html,
256
+ settings.header?.html,
257
+ settings.footer?.html
258
+ ]);
206
259
  return {
207
260
  slug,
208
261
  dir,
@@ -213,8 +266,14 @@ function readTemplate(project, slug) {
213
266
  head: file ? "" : readText(join2(dir, "head.html")) ?? "",
214
267
  settings,
215
268
  dataSets,
216
- dataSchema: readJson(join2(dir, "schema.json"), "schema.json"),
217
- i18n: readJson(join2(dir, "i18n.json"), "i18n.json"),
269
+ dataSchema: readJson(
270
+ join2(dir, "schema.json"),
271
+ "schema.json"
272
+ ),
273
+ i18n: readJson(
274
+ join2(dir, "i18n.json"),
275
+ "i18n.json"
276
+ ),
218
277
  partials,
219
278
  missingPartials: missing,
220
279
  sharedPartials: shared
@@ -224,35 +283,68 @@ function defaultData(tpl, name) {
224
283
  const names = Object.keys(tpl.dataSets);
225
284
  const chosen = name ?? (names.includes("default") ? "default" : names[0]);
226
285
  if (name && !(name in tpl.dataSets))
227
- throw new DevkitError(`Data set "${name}" not found; available: ${names.join(", ") || "none"}`);
286
+ throw new DevkitError(
287
+ `Data set "${name}" not found; available: ${names.join(", ") || "none"}`
288
+ );
228
289
  return chosen ? { name: chosen, data: tpl.dataSets[chosen] } : { name: "empty", data: {} };
229
290
  }
230
291
  function writeTemplate(project, slug, meta, version, file) {
231
292
  const dir = templateDir(project, slug);
232
293
  const office = isOfficeKind(meta.kind);
233
- if (office && !file) throw new DevkitError(`${slug} is a ${meta.kind} template; its document is needed to write the folder`);
294
+ if (office && !file)
295
+ throw new DevkitError(
296
+ `${slug} is a ${meta.kind} template; its document is needed to write the folder`
297
+ );
234
298
  mkdirSync(join2(dir, "data"), { recursive: true });
235
299
  const settings = { ...version.settings ?? {} };
236
300
  const header = office ? "" : settings.header?.html ?? "";
237
301
  const footer = office ? "" : settings.footer?.html ?? "";
238
302
  if (settings.header) settings.header = stripHtml(settings.header);
239
303
  if (settings.footer) settings.footer = stripHtml(settings.footer);
240
- writeFileSync2(join2(dir, "template.json"), JSON.stringify(meta, null, 2) + "\n");
241
- for (const name of ["template.html", ...officeKinds.map((k) => `template.${k}`)])
242
- if (name !== templateFileName(meta.kind) && existsSync2(join2(dir, name))) rmSync(join2(dir, name));
304
+ const design = settings.design;
305
+ delete settings.design;
306
+ writeFileSync2(
307
+ join2(dir, "template.json"),
308
+ JSON.stringify(meta, null, 2) + "\n"
309
+ );
310
+ for (const name of [
311
+ "template.html",
312
+ ...officeKinds.map((k) => `template.${k}`)
313
+ ])
314
+ if (name !== templateFileName(meta.kind) && existsSync2(join2(dir, name)))
315
+ rmSync(join2(dir, name));
243
316
  if (office) writeFileSync2(join2(dir, templateFileName(meta.kind)), file);
244
317
  else writeFileSync2(join2(dir, "template.html"), version.html ?? "");
245
318
  writeOrRemove(join2(dir, "style.css"), office ? "" : version.css ?? "");
246
319
  writeOrRemove(join2(dir, "head.html"), office ? "" : version.head ?? "");
247
320
  writeOrRemove(join2(dir, "header.html"), header);
248
321
  writeOrRemove(join2(dir, "footer.html"), footer);
249
- writeFileSync2(join2(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
250
- writeFileSync2(join2(dir, "data", "default.json"), JSON.stringify(version.sample_data ?? {}, null, 2) + "\n");
322
+ writeFileSync2(
323
+ join2(dir, "settings.json"),
324
+ JSON.stringify(settings, null, 2) + "\n"
325
+ );
326
+ writeOrRemove(
327
+ join2(dir, "design.json"),
328
+ design && typeof design === "object" ? JSON.stringify(design, null, 2) + "\n" : ""
329
+ );
330
+ writeFileSync2(
331
+ join2(dir, "data", "default.json"),
332
+ JSON.stringify(version.sample_data ?? {}, null, 2) + "\n"
333
+ );
251
334
  for (const [name, data] of Object.entries(version.data_sets ?? {}))
252
335
  if (name !== "default" && /^[\w.-]+$/.test(name))
253
- writeFileSync2(join2(dir, "data", `${name}.json`), JSON.stringify(data ?? {}, null, 2) + "\n");
254
- writeOrRemove(join2(dir, "schema.json"), version.data_schema ? JSON.stringify(version.data_schema, null, 2) + "\n" : "");
255
- writeOrRemove(join2(dir, "i18n.json"), version.i18n ? JSON.stringify(version.i18n, null, 2) + "\n" : "");
336
+ writeFileSync2(
337
+ join2(dir, "data", `${name}.json`),
338
+ JSON.stringify(data ?? {}, null, 2) + "\n"
339
+ );
340
+ writeOrRemove(
341
+ join2(dir, "schema.json"),
342
+ version.data_schema ? JSON.stringify(version.data_schema, null, 2) + "\n" : ""
343
+ );
344
+ writeOrRemove(
345
+ join2(dir, "i18n.json"),
346
+ version.i18n ? JSON.stringify(version.i18n, null, 2) + "\n" : ""
347
+ );
256
348
  writePartials(project, version.partials ?? null);
257
349
  return dir;
258
350
  }
@@ -261,7 +353,10 @@ function writePartials(project, partials) {
261
353
  const changed = [];
262
354
  for (const [name, source] of Object.entries(partials ?? {})) {
263
355
  if (!partialName.test(name)) continue;
264
- const path = join2(project.partialsDir, name.includes(".") ? name : `${name}.html`);
356
+ const path = join2(
357
+ project.partialsDir,
358
+ name.includes(".") ? name : `${name}.html`
359
+ );
265
360
  if (readText(path) === source) continue;
266
361
  mkdirSync(dirname2(path), { recursive: true });
267
362
  writeFileSync2(path, source);
@@ -307,17 +402,36 @@ function versionPayload(tpl) {
307
402
  function contentHash(tpl) {
308
403
  if (tpl.file) {
309
404
  const payload2 = officeVersionPayload(tpl);
310
- const parts2 = [tpl.file.format, tpl.file.sha256, payload2.settings, payload2.sample_data, payload2.data_sets, payload2.data_schema, payload2.i18n];
405
+ const parts2 = [
406
+ tpl.file.format,
407
+ tpl.file.sha256,
408
+ payload2.settings,
409
+ payload2.sample_data,
410
+ payload2.data_sets,
411
+ payload2.data_schema,
412
+ payload2.i18n
413
+ ];
311
414
  return createHash("sha256").update(JSON.stringify(parts2)).digest("hex");
312
415
  }
313
416
  const payload = versionPayload(tpl);
314
- const parts = [payload.html, payload.css, payload.head, payload.settings, payload.sample_data, payload.data_sets, payload.data_schema, payload.i18n];
417
+ const parts = [
418
+ payload.html,
419
+ payload.css,
420
+ payload.head,
421
+ payload.settings,
422
+ payload.sample_data,
423
+ payload.data_sets,
424
+ payload.data_schema,
425
+ payload.i18n
426
+ ];
315
427
  if (payload.partials) parts.push(payload.partials);
316
428
  return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
317
429
  }
318
430
  var statePath = (project) => join2(project.root, ".formfeed", "state.json");
319
431
  function readState(project) {
320
- return readJson(statePath(project), ".formfeed/state.json") ?? { templates: {} };
432
+ return readJson(statePath(project), ".formfeed/state.json") ?? {
433
+ templates: {}
434
+ };
321
435
  }
322
436
  function writeState(project, state) {
323
437
  mkdirSync(join2(project.root, ".formfeed"), { recursive: true });
@@ -343,7 +457,8 @@ function partialResolver(project) {
343
457
  return (name) => {
344
458
  for (const candidate of [name, `${name}.html`]) {
345
459
  const path = join2(project.partialsDir, candidate);
346
- if (existsSync2(path) && statSync(path).isFile()) return readFileSync2(path, "utf8");
460
+ if (existsSync2(path) && statSync(path).isFile())
461
+ return readFileSync2(path, "utf8");
347
462
  }
348
463
  shared ??= new Set(Object.keys(readState(project).sharedPartials ?? {}));
349
464
  const normalised = normalisePartialName2(name);
@@ -364,7 +479,12 @@ function recordSharedPartial(project, name, remote, source) {
364
479
  const state = readState(project);
365
480
  state.sharedPartials = {
366
481
  ...state.sharedPartials ?? {},
367
- [name]: { version: remote.version, engine: remote.engine, contentHash: partialContentHash(source), syncedAt: (/* @__PURE__ */ new Date()).toISOString() }
482
+ [name]: {
483
+ version: remote.version,
484
+ engine: remote.engine,
485
+ contentHash: partialContentHash(source),
486
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString()
487
+ }
368
488
  };
369
489
  writeState(project, state);
370
490
  }
@@ -414,12 +534,14 @@ import {
414
534
  analyzeOffice,
415
535
  defaultHelpers,
416
536
  defaultLimits,
537
+ designOutputHash,
417
538
  flowDocument,
418
539
  getEngine as getEngine2,
419
540
  isOfficeKind as isOfficeKind2,
420
541
  mergeSettings,
421
542
  officeTextParts,
422
543
  pagedDocument,
544
+ parseDesign,
423
545
  renderOffice,
424
546
  renderVersion
425
547
  } from "@formfeed/engine";
@@ -435,7 +557,10 @@ function diagnose(tpl, sampleData) {
435
557
  severity: "error",
436
558
  code: "syntax-error",
437
559
  message: e.message,
438
- range: { start: { line: e.line ?? 1, column: e.column ?? 1 }, end: { line: e.line ?? 1, column: (e.column ?? 1) + 1 } }
560
+ range: {
561
+ start: { line: e.line ?? 1, column: e.column ?? 1 },
562
+ end: { line: e.line ?? 1, column: (e.column ?? 1) + 1 }
563
+ }
439
564
  });
440
565
  } else throw e;
441
566
  }
@@ -445,7 +570,8 @@ function diagnose(tpl, sampleData) {
445
570
  ["footer", tpl.settings.footer?.html]
446
571
  ]) {
447
572
  if (!source) continue;
448
- for (const d of engine.analyze(source, { sampleData }).diagnostics) diagnostics.push({ ...d, message: `${part}: ${d.message}` });
573
+ for (const d of engine.analyze(source, { sampleData }).diagnostics)
574
+ diagnostics.push({ ...d, message: `${part}: ${d.message}` });
449
575
  }
450
576
  for (const name of tpl.missingPartials)
451
577
  diagnostics.push({
@@ -455,8 +581,28 @@ function diagnose(tpl, sampleData) {
455
581
  message: `No partial "${name}" in the project's partials folder; if it is shared by the organisation, formfeed partials pull brings it`,
456
582
  range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }
457
583
  });
584
+ const design = designDiagnostic(tpl);
585
+ if (design) diagnostics.push(design);
458
586
  return diagnostics;
459
587
  }
588
+ function designDiagnostic(tpl) {
589
+ if (tpl.settings.design === void 0) return null;
590
+ const parsed = parseDesign(tpl.settings.design);
591
+ if (!parsed.design)
592
+ return {
593
+ severity: "warning",
594
+ code: "design-invalid",
595
+ message: `design.json is not a design the app can open (${parsed.problems.slice(0, 3).join("; ")}); the template opens as code`,
596
+ range: nowhere
597
+ };
598
+ if (parsed.design.output === designOutputHash(tpl.html, tpl.css)) return null;
599
+ return {
600
+ severity: "warning",
601
+ code: "design-stale",
602
+ 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",
603
+ range: nowhere
604
+ };
605
+ }
460
606
  var nowhere = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };
461
607
  function diagnoseOffice(tpl, file, sampleData) {
462
608
  let analysis;
@@ -464,7 +610,14 @@ function diagnoseOffice(tpl, file, sampleData) {
464
610
  analysis = analyzeOffice(file, { engine: tpl.meta.engine, sampleData });
465
611
  } catch (e) {
466
612
  const code = e.code;
467
- return [{ severity: "error", code: typeof code === "string" ? code : "office-document-invalid", message: e instanceof Error ? e.message : String(e), range: nowhere }];
613
+ return [
614
+ {
615
+ severity: "error",
616
+ code: typeof code === "string" ? code : "office-document-invalid",
617
+ message: e instanceof Error ? e.message : String(e),
618
+ range: nowhere
619
+ }
620
+ ];
468
621
  }
469
622
  return analysis.diagnostics.map((d) => ({
470
623
  severity: d.severity,
@@ -476,17 +629,34 @@ function diagnoseOffice(tpl, file, sampleData) {
476
629
  }));
477
630
  }
478
631
  async function renderOfficeLocal(project, tpl, data, options = {}) {
479
- if (!tpl.file) throw new DevkitError(`${tpl.slug} is not a Word or PowerPoint template`, "validation");
480
- const context = renderContext(project, tpl, options.assetBaseUrl, options.brand);
632
+ if (!tpl.file)
633
+ throw new DevkitError(
634
+ `${tpl.slug} is not a Word or PowerPoint template`,
635
+ "validation"
636
+ );
637
+ const context = renderContext(
638
+ project,
639
+ tpl,
640
+ options.assetBaseUrl,
641
+ options.brand
642
+ );
481
643
  if (options.locale) context.locale = options.locale;
482
- return renderOffice(tpl.file.bytes, { engine: tpl.meta.engine, data, context, images: options.images, random: options.random });
644
+ return renderOffice(tpl.file.bytes, {
645
+ engine: tpl.meta.engine,
646
+ data,
647
+ context,
648
+ images: options.images,
649
+ random: options.random
650
+ });
483
651
  }
484
652
  function htmlSnapshot(rendered) {
485
653
  let snapshot = rendered.document;
486
- if (rendered.headerHtml) snapshot += `
654
+ if (rendered.headerHtml)
655
+ snapshot += `
487
656
  <!-- formfeed:header -->
488
657
  ${rendered.headerHtml}`;
489
- if (rendered.footerHtml) snapshot += `
658
+ if (rendered.footerHtml)
659
+ snapshot += `
490
660
  <!-- formfeed:footer -->
491
661
  ${rendered.footerHtml}`;
492
662
  return snapshot;
@@ -539,7 +709,14 @@ async function renderLocal(project, tpl, data, options) {
539
709
  const ctx = renderContext(project, tpl, options.assetBaseUrl, options.brand);
540
710
  if (options.locale) ctx.locale = options.locale;
541
711
  return renderVersion(
542
- { engine: tpl.meta.engine, html: tpl.html, css: tpl.css, head: tpl.head, settings: mergeSettings(tpl.settings), kind },
712
+ {
713
+ engine: tpl.meta.engine,
714
+ html: tpl.html,
715
+ css: tpl.css,
716
+ head: tpl.head,
717
+ settings: mergeSettings(tpl.settings),
718
+ kind
719
+ },
543
720
  data,
544
721
  ctx,
545
722
  options.mode,
@@ -549,16 +726,26 @@ async function renderLocal(project, tpl, data, options) {
549
726
  function renderedSettings(rendered) {
550
727
  const style = rendered.inlineCss ? `<style>${rendered.inlineCss}</style>` : "";
551
728
  const part = (settings2, html) => settings2?.html ? { ...settings2, html: `${style}${html ?? ""}` } : settings2;
552
- const { settings } = rendered;
729
+ const { design: _design, ...settings } = rendered.settings;
730
+ void _design;
553
731
  return {
554
732
  ...settings,
555
733
  ...settings.header ? { header: part(settings.header, rendered.headerHtml) } : {},
556
734
  ...settings.footer ? { footer: part(settings.footer, rendered.footerHtml) } : {},
557
- ...settings.pdf?.metadata?.title !== void 0 ? { pdf: { ...settings.pdf, metadata: { ...settings.pdf.metadata, title: rendered.title ?? "" } } } : {}
735
+ ...settings.pdf?.metadata?.title !== void 0 ? {
736
+ pdf: {
737
+ ...settings.pdf,
738
+ metadata: { ...settings.pdf.metadata, title: rendered.title ?? "" }
739
+ }
740
+ } : {}
558
741
  };
559
742
  }
560
743
  function htmlKind(tpl) {
561
- if (isOfficeKind2(tpl.meta.kind)) throw new DevkitError(`${tpl.slug} is a ${tpl.meta.kind} template; fill it with renderOfficeLocal`, "validation");
744
+ if (isOfficeKind2(tpl.meta.kind))
745
+ throw new DevkitError(
746
+ `${tpl.slug} is a ${tpl.meta.kind} template; fill it with renderOfficeLocal`,
747
+ "validation"
748
+ );
562
749
  return tpl.meta.kind;
563
750
  }
564
751
  function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
@@ -573,10 +760,109 @@ function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
573
760
  return mode === "paged" && kind === "pdf" ? pagedDocument(draft, { pagedScriptUrl }) : flowDocument(draft);
574
761
  }
575
762
 
763
+ // packages/devkit/src/lib/einvoice.ts
764
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
765
+ import { join as join4 } from "node:path";
766
+ import {
767
+ INVOICE_DATA_KEY,
768
+ einvoiceXml,
769
+ invoicePathSegments,
770
+ locateJsonPath,
771
+ resolveEinvoiceOptions
772
+ } from "@formfeed/engine";
773
+ function declaredEinvoice(tpl, override) {
774
+ const resolved = resolveEinvoiceOptions(tpl.settings.einvoice, override);
775
+ if (!resolved.ok)
776
+ throw new DevkitError(
777
+ `${tpl.slug}: e-invoice ${resolved.field} "${resolved.value}": ${resolved.reason}`,
778
+ "validation"
779
+ );
780
+ return resolved.options;
781
+ }
782
+ var nowhere2 = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };
783
+ function diagnoseEinvoice(tpl) {
784
+ let options;
785
+ try {
786
+ options = declaredEinvoice(tpl);
787
+ } catch (e) {
788
+ return [
789
+ {
790
+ dataSet: "",
791
+ file: "settings.json",
792
+ diagnostics: [
793
+ {
794
+ severity: "error",
795
+ code: "einvoice-profile",
796
+ message: e instanceof Error ? e.message : String(e),
797
+ range: nowhere2
798
+ }
799
+ ]
800
+ }
801
+ ];
802
+ }
803
+ if (!options) return [];
804
+ if (tpl.meta.kind === "image")
805
+ return [
806
+ {
807
+ dataSet: "",
808
+ file: "settings.json",
809
+ diagnostics: [
810
+ {
811
+ severity: "warning",
812
+ code: "einvoice-image",
813
+ message: "settings.json declares an e-invoice, which an image template never produces: an e-invoice is a PDF",
814
+ range: nowhere2
815
+ }
816
+ ]
817
+ }
818
+ ];
819
+ const out = [];
820
+ for (const [name, data] of Object.entries(tpl.dataSets)) {
821
+ const result = einvoiceXml(data, options);
822
+ if (result.ok) continue;
823
+ const file = `data/${name}.json`;
824
+ const path = join4(tpl.dir, "data", `${name}.json`);
825
+ const text2 = existsSync4(path) ? readFileSync4(path, "utf8") : null;
826
+ out.push({
827
+ dataSet: name,
828
+ file,
829
+ diagnostics: result.problems.map((problem) => {
830
+ const at = text2 ? locateJsonPath(text2, [
831
+ INVOICE_DATA_KEY,
832
+ ...invoicePathSegments(problem.path)
833
+ ]) : null;
834
+ const start = at ? { line: at.line, column: at.column } : nowhere2.start;
835
+ return {
836
+ severity: "error",
837
+ code: `einvoice-${problem.code.replace(/_/g, "-")}`,
838
+ message: problem.message,
839
+ range: { start, end: start }
840
+ };
841
+ })
842
+ });
843
+ }
844
+ return out;
845
+ }
846
+ function localEinvoiceXml(tpl, data, override) {
847
+ const options = declaredEinvoice(tpl, override ?? {});
848
+ if (!options)
849
+ throw new DevkitError(`${tpl.slug} declares no e-invoice`, "validation");
850
+ const result = einvoiceXml(data, options);
851
+ if (!result.ok)
852
+ throw new DevkitError(
853
+ [
854
+ `${tpl.slug}: the _invoice block does not make an e-invoice`,
855
+ ...result.problems.map((problem) => ` ${problem.message}`)
856
+ ].join("\n"),
857
+ "validation"
858
+ );
859
+ return { xml: result.xml, invoice: result.invoice, options };
860
+ }
861
+
576
862
  // packages/devkit/src/lib/files.ts
577
863
  import { createHash as createHash2 } from "node:crypto";
578
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
579
- import { dirname as dirname3, join as join4, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
864
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
865
+ import { dirname as dirname3, join as join5, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
580
866
 
581
867
  // packages/api-types/src/lib/assets.ts
582
868
  var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
@@ -608,11 +894,11 @@ function sha256File(bytes) {
608
894
  }
609
895
  function listLocalFiles(project) {
610
896
  const listing = { files: [], skipped: [] };
611
- if (!existsSync4(project.filesDir)) return listing;
897
+ if (!existsSync5(project.filesDir)) return listing;
612
898
  const walk2 = (dir) => {
613
899
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
614
900
  if (entry.name.startsWith(".")) continue;
615
- const path = join4(dir, entry.name);
901
+ const path = join5(dir, entry.name);
616
902
  if (entry.isDirectory()) {
617
903
  walk2(path);
618
904
  continue;
@@ -626,7 +912,7 @@ function listLocalFiles(project) {
626
912
  listing.skipped.push({ path, reason });
627
913
  continue;
628
914
  }
629
- const bytes = readFileSync4(path);
915
+ const bytes = readFileSync5(path);
630
916
  listing.files.push({ name, path, bytes: size, sha256: sha256File(bytes), contentType });
631
917
  }
632
918
  };
@@ -1210,10 +1496,12 @@ export {
1210
1496
  collectPartials,
1211
1497
  contentHash,
1212
1498
  contentTypeFor,
1499
+ declaredEinvoice,
1213
1500
  defaultData,
1214
1501
  defaultProjectConfig,
1215
1502
  defaultTypesFile,
1216
1503
  diagnose,
1504
+ diagnoseEinvoice,
1217
1505
  findProject,
1218
1506
  generateTypes,
1219
1507
  htmlSnapshot,
@@ -1221,6 +1509,7 @@ export {
1221
1509
  listLocalFiles,
1222
1510
  listTemplateSlugs,
1223
1511
  loadProject,
1512
+ localEinvoiceXml,
1224
1513
  localFilePath,
1225
1514
  localTypeSource,
1226
1515
  officeSnapshot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formfeed/devkit",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Formfeed template folders, project config and local rendering, shared by the CLI and @formfeed/testing.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,8 +30,8 @@
30
30
  ],
31
31
  "sideEffects": false,
32
32
  "dependencies": {
33
- "@formfeed/engine": "0.3.10",
34
- "@formfeed/sdk": "0.3.10"
33
+ "@formfeed/engine": "0.3.12",
34
+ "@formfeed/sdk": "0.3.12"
35
35
  },
36
36
  "keywords": [
37
37
  "formfeed",
package/types/index.d.cts CHANGED
@@ -1,16 +1,18 @@
1
1
  export { DevkitError } from './lib/errors.cjs';
2
- export { defaultProjectConfig, findProject, isIgnored, loadProject, projectAround, writeProjectConfig } from './lib/project-config.cjs';
2
+ export { defaultProjectConfig, findProject, isIgnored, loadProject, projectAround, writeProjectConfig, } from './lib/project-config.cjs';
3
3
  export type { Project, ProjectConfig } from './lib/project-config.cjs';
4
4
  export { contentHash, defaultData, listTemplateSlugs, officeVersionPayload, partialContentHash, partialResolver, readState, readTemplate, recordSharedPartial, recordSync, sharedPartialNames, sharedPartialPath, templateDir, templateFileName, titleFromSlug, versionPayload, writePartials, writeState, writeTemplate, } from './lib/project.cjs';
5
- export type { LocalOfficeFile, LocalTemplate, ProjectState, SharedPartialState, TemplateMeta, TemplateState } from './lib/project.cjs';
5
+ export type { LocalOfficeFile, LocalTemplate, ProjectState, SharedPartialState, TemplateMeta, TemplateState, } from './lib/project.cjs';
6
6
  export { brandFromJson, brandPath, readBrand, writeBrand } from './lib/brand.cjs';
7
7
  export { collectPartials } from './lib/partials.cjs';
8
8
  export type { CollectedPartials } from './lib/partials.cjs';
9
- export { diagnose, htmlSnapshot, officeSnapshot, prettyXml, previewDocument, renderContext, renderLocal, renderOfficeLocal, renderedSettings } from './lib/local-render.cjs';
9
+ export { diagnose, htmlSnapshot, officeSnapshot, prettyXml, previewDocument, renderContext, renderLocal, renderOfficeLocal, renderedSettings, } from './lib/local-render.cjs';
10
10
  export type { LocalDiagnostic, OfficeLocalOptions } from './lib/local-render.cjs';
11
- export { contentTypeFor, listLocalFiles, localFilePath, sha256File, writeLocalFile } from './lib/files.cjs';
11
+ export { declaredEinvoice, diagnoseEinvoice, localEinvoiceXml, } from './lib/einvoice.cjs';
12
+ export type { DataSetDiagnostics } from './lib/einvoice.cjs';
13
+ export { contentTypeFor, listLocalFiles, localFilePath, sha256File, writeLocalFile, } from './lib/files.cjs';
12
14
  export type { LocalFile, LocalFileListing } from './lib/files.cjs';
13
15
  export type { LocalRenderOptions } from './lib/local-render.cjs';
14
16
  export { parseRedactPath, redactData, redactString } from './lib/redact.cjs';
15
17
  export { defaultTypesFile, generateTypes, localTypeSource } from './lib/types/index.cjs';
16
- export type { SchemaOrigin, TypeScriptOptions, TypeSource, TypesLanguage } from './lib/types/index.cjs';
18
+ export type { SchemaOrigin, TypeScriptOptions, TypeSource, TypesLanguage, } from './lib/types/index.cjs';