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