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