@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 +334 -58
- package/index.js +339 -50
- package/package.json +3 -3
- package/types/index.d.cts +7 -5
- package/types/index.d.ts +7 -5
- package/types/lib/einvoice.d.cts +37 -0
- package/types/lib/einvoice.d.ts +37 -0
- package/types/lib/project.d.cts +2 -1
- package/types/lib/project.d.ts +2 -1
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 = {
|
|
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())
|
|
197
|
-
|
|
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(
|
|
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, [
|
|
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(
|
|
217
|
-
|
|
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(
|
|
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)
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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(
|
|
250
|
-
|
|
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(
|
|
254
|
-
|
|
255
|
-
|
|
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(
|
|
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 = [
|
|
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 = [
|
|
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") ?? {
|
|
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())
|
|
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]: {
|
|
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: {
|
|
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)
|
|
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 [
|
|
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)
|
|
480
|
-
|
|
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, {
|
|
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)
|
|
654
|
+
if (rendered.headerHtml)
|
|
655
|
+
snapshot += `
|
|
487
656
|
<!-- formfeed:header -->
|
|
488
657
|
${rendered.headerHtml}`;
|
|
489
|
-
if (rendered.footerHtml)
|
|
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
|
-
{
|
|
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 ? {
|
|
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))
|
|
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
|
|
579
|
-
import { dirname as dirname3, join as
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
34
|
-
"@formfeed/sdk": "0.3.
|
|
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 {
|
|
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';
|