@formfeed/devkit 0.2.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 +818 -56
- package/index.js +797 -46
- package/package.json +3 -3
- package/types/index.d.cts +8 -3
- package/types/index.d.ts +8 -3
- package/types/lib/brand.d.cts +14 -0
- package/types/lib/brand.d.ts +14 -0
- package/types/lib/local-render.d.cts +32 -3
- package/types/lib/local-render.d.ts +32 -3
- package/types/lib/partials.d.cts +2 -0
- package/types/lib/partials.d.ts +2 -0
- package/types/lib/project-config.d.cts +13 -0
- package/types/lib/project-config.d.ts +13 -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.cjs
CHANGED
|
@@ -21,29 +21,48 @@ 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,
|
|
24
26
|
collectPartials: () => collectPartials,
|
|
25
27
|
contentHash: () => contentHash,
|
|
26
28
|
contentTypeFor: () => contentTypeFor,
|
|
27
29
|
defaultData: () => defaultData,
|
|
28
30
|
defaultProjectConfig: () => defaultProjectConfig,
|
|
31
|
+
defaultTypesFile: () => defaultTypesFile,
|
|
29
32
|
diagnose: () => diagnose,
|
|
30
33
|
findProject: () => findProject,
|
|
34
|
+
generateTypes: () => generateTypes,
|
|
31
35
|
listLocalFiles: () => listLocalFiles,
|
|
32
36
|
listTemplateSlugs: () => listTemplateSlugs,
|
|
33
37
|
loadProject: () => loadProject,
|
|
34
38
|
localFilePath: () => localFilePath,
|
|
39
|
+
localTypeSource: () => localTypeSource,
|
|
40
|
+
officeSnapshot: () => officeSnapshot,
|
|
41
|
+
officeVersionPayload: () => officeVersionPayload,
|
|
42
|
+
parseRedactPath: () => parseRedactPath,
|
|
43
|
+
partialContentHash: () => partialContentHash,
|
|
35
44
|
partialResolver: () => partialResolver,
|
|
45
|
+
prettyXml: () => prettyXml,
|
|
36
46
|
previewDocument: () => previewDocument,
|
|
37
47
|
projectAround: () => projectAround,
|
|
48
|
+
readBrand: () => readBrand,
|
|
38
49
|
readState: () => readState,
|
|
39
50
|
readTemplate: () => readTemplate,
|
|
51
|
+
recordSharedPartial: () => recordSharedPartial,
|
|
40
52
|
recordSync: () => recordSync,
|
|
53
|
+
redactData: () => redactData,
|
|
54
|
+
redactString: () => redactString,
|
|
41
55
|
renderContext: () => renderContext,
|
|
42
56
|
renderLocal: () => renderLocal,
|
|
57
|
+
renderOfficeLocal: () => renderOfficeLocal,
|
|
43
58
|
sha256File: () => sha256File,
|
|
59
|
+
sharedPartialNames: () => sharedPartialNames,
|
|
60
|
+
sharedPartialPath: () => sharedPartialPath,
|
|
44
61
|
templateDir: () => templateDir,
|
|
62
|
+
templateFileName: () => templateFileName,
|
|
45
63
|
titleFromSlug: () => titleFromSlug,
|
|
46
64
|
versionPayload: () => versionPayload,
|
|
65
|
+
writeBrand: () => writeBrand,
|
|
47
66
|
writeLocalFile: () => writeLocalFile,
|
|
48
67
|
writePartials: () => writePartials,
|
|
49
68
|
writeProjectConfig: () => writeProjectConfig,
|
|
@@ -125,6 +144,7 @@ function writeProjectConfig(root, config) {
|
|
|
125
144
|
var import_node_crypto = require("node:crypto");
|
|
126
145
|
var import_node_fs2 = require("node:fs");
|
|
127
146
|
var import_node_path2 = require("node:path");
|
|
147
|
+
var import_engine2 = require("@formfeed/engine");
|
|
128
148
|
|
|
129
149
|
// packages/devkit/src/lib/partials.ts
|
|
130
150
|
var import_engine = require("@formfeed/engine");
|
|
@@ -137,8 +157,10 @@ function includeNames(engine, source) {
|
|
|
137
157
|
}
|
|
138
158
|
function collectPartials(project, engine, sources) {
|
|
139
159
|
const resolve3 = partialResolver(project);
|
|
160
|
+
const sharedNames = sharedPartialNames(project, engine);
|
|
140
161
|
const partials = {};
|
|
141
162
|
const missing = /* @__PURE__ */ new Set();
|
|
163
|
+
const shared = /* @__PURE__ */ new Set();
|
|
142
164
|
const queue = sources.filter((source) => Boolean(source));
|
|
143
165
|
const seen = /* @__PURE__ */ new Set();
|
|
144
166
|
while (queue.length) {
|
|
@@ -146,6 +168,11 @@ function collectPartials(project, engine, sources) {
|
|
|
146
168
|
if (seen.has(name)) continue;
|
|
147
169
|
seen.add(name);
|
|
148
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
|
+
}
|
|
149
176
|
if (source === void 0) {
|
|
150
177
|
missing.add(name);
|
|
151
178
|
continue;
|
|
@@ -154,65 +181,86 @@ function collectPartials(project, engine, sources) {
|
|
|
154
181
|
queue.push(source);
|
|
155
182
|
}
|
|
156
183
|
}
|
|
157
|
-
return { partials, missing: [...missing].sort() };
|
|
184
|
+
return { partials, missing: [...missing].sort(), shared: [...shared].sort() };
|
|
158
185
|
}
|
|
159
186
|
|
|
160
187
|
// packages/devkit/src/lib/project.ts
|
|
161
188
|
var readText = (path) => (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : null;
|
|
162
189
|
function readJson(path, what) {
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
190
|
+
const text2 = readText(path);
|
|
191
|
+
if (text2 === null) return null;
|
|
165
192
|
try {
|
|
166
|
-
return JSON.parse(
|
|
193
|
+
return JSON.parse(text2);
|
|
167
194
|
} catch (e) {
|
|
168
195
|
throw new DevkitError(`${what} is not valid JSON: ${path} (${e instanceof Error ? e.message : e})`, "validation");
|
|
169
196
|
}
|
|
170
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
|
+
}
|
|
171
202
|
function listTemplateSlugs(project) {
|
|
172
203
|
if (!(0, import_node_fs2.existsSync)(project.templatesDir)) return [];
|
|
173
|
-
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();
|
|
174
205
|
}
|
|
175
206
|
function templateDir(project, slug) {
|
|
176
207
|
return (0, import_node_path2.join)(project.templatesDir, slug);
|
|
177
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
|
+
}
|
|
178
215
|
function readTemplate(project, slug) {
|
|
179
216
|
const dir = templateDir(project, slug);
|
|
180
|
-
const
|
|
181
|
-
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")) ?? "";
|
|
182
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");
|
|
183
229
|
const meta = {
|
|
184
230
|
name: metaFile.name ?? titleFromSlug(slug),
|
|
185
|
-
kind: metaFile.kind ?? "pdf",
|
|
231
|
+
kind: file?.format ?? metaFile.kind ?? "pdf",
|
|
186
232
|
engine: metaFile.engine ?? project.config.engine,
|
|
187
233
|
description: metaFile.description ?? null,
|
|
188
234
|
tags: metaFile.tags ?? []
|
|
189
235
|
};
|
|
190
236
|
const settings = { ...readJson((0, import_node_path2.join)(dir, "settings.json"), "settings.json") ?? {} };
|
|
191
|
-
const header = readText((0, import_node_path2.join)(dir, "header.html"));
|
|
192
|
-
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"));
|
|
193
239
|
if (header !== null && header.trim()) settings.header = { ...settings.header ?? {}, html: header };
|
|
194
240
|
if (footer !== null && footer.trim()) settings.footer = { ...settings.footer ?? {}, html: footer };
|
|
195
241
|
const dataSets = {};
|
|
196
242
|
const dataDir = (0, import_node_path2.join)(dir, "data");
|
|
197
243
|
if ((0, import_node_fs2.existsSync)(dataDir)) {
|
|
198
|
-
for (const
|
|
199
|
-
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}`);
|
|
200
246
|
}
|
|
201
247
|
}
|
|
202
|
-
const { partials, missing } = collectPartials(project, meta.engine, [html, settings.header?.html, settings.footer?.html]);
|
|
248
|
+
const { partials, missing, shared } = file ? { partials: {}, missing: [], shared: [] } : collectPartials(project, meta.engine, [html, settings.header?.html, settings.footer?.html]);
|
|
203
249
|
return {
|
|
204
250
|
slug,
|
|
205
251
|
dir,
|
|
206
252
|
meta,
|
|
253
|
+
file,
|
|
207
254
|
html,
|
|
208
|
-
css: readText((0, import_node_path2.join)(dir, "style.css")) ?? "",
|
|
209
|
-
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")) ?? "",
|
|
210
257
|
settings,
|
|
211
258
|
dataSets,
|
|
212
259
|
dataSchema: readJson((0, import_node_path2.join)(dir, "schema.json"), "schema.json"),
|
|
213
260
|
i18n: readJson((0, import_node_path2.join)(dir, "i18n.json"), "i18n.json"),
|
|
214
261
|
partials,
|
|
215
|
-
missingPartials: missing
|
|
262
|
+
missingPartials: missing,
|
|
263
|
+
sharedPartials: shared
|
|
216
264
|
};
|
|
217
265
|
}
|
|
218
266
|
function defaultData(tpl, name) {
|
|
@@ -222,18 +270,23 @@ function defaultData(tpl, name) {
|
|
|
222
270
|
throw new DevkitError(`Data set "${name}" not found; available: ${names.join(", ") || "none"}`);
|
|
223
271
|
return chosen ? { name: chosen, data: tpl.dataSets[chosen] } : { name: "empty", data: {} };
|
|
224
272
|
}
|
|
225
|
-
function writeTemplate(project, slug, meta, version) {
|
|
273
|
+
function writeTemplate(project, slug, meta, version, file) {
|
|
226
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`);
|
|
227
277
|
(0, import_node_fs2.mkdirSync)((0, import_node_path2.join)(dir, "data"), { recursive: true });
|
|
228
278
|
const settings = { ...version.settings ?? {} };
|
|
229
|
-
const header = settings.header?.html ?? "";
|
|
230
|
-
const footer = settings.footer?.html ?? "";
|
|
279
|
+
const header = office ? "" : settings.header?.html ?? "";
|
|
280
|
+
const footer = office ? "" : settings.footer?.html ?? "";
|
|
231
281
|
if (settings.header) settings.header = stripHtml(settings.header);
|
|
232
282
|
if (settings.footer) settings.footer = stripHtml(settings.footer);
|
|
233
283
|
(0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "template.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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 ?? "");
|
|
237
290
|
writeOrRemove((0, import_node_path2.join)(dir, "header.html"), header);
|
|
238
291
|
writeOrRemove((0, import_node_path2.join)(dir, "footer.html"), footer);
|
|
239
292
|
(0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
|
|
@@ -268,25 +321,38 @@ function writeOrRemove(path, content) {
|
|
|
268
321
|
if (content) (0, import_node_fs2.writeFileSync)(path, content);
|
|
269
322
|
else if ((0, import_node_fs2.existsSync)(path)) (0, import_node_fs2.rmSync)(path);
|
|
270
323
|
}
|
|
271
|
-
function
|
|
324
|
+
function commonPayload(tpl) {
|
|
272
325
|
const sample = tpl.dataSets["default"] ?? Object.values(tpl.dataSets)[0] ?? {};
|
|
273
326
|
const named = Object.fromEntries(
|
|
274
327
|
Object.entries(tpl.dataSets).filter(([name]) => name !== "default")
|
|
275
328
|
);
|
|
276
329
|
return {
|
|
277
|
-
html: tpl.html,
|
|
278
|
-
css: tpl.css,
|
|
279
|
-
head: tpl.head,
|
|
280
330
|
settings: tpl.settings,
|
|
281
331
|
sample_data: sample ?? {},
|
|
282
332
|
data_sets: named,
|
|
283
333
|
data_schema: tpl.dataSchema,
|
|
284
|
-
i18n: tpl.i18n
|
|
334
|
+
i18n: tpl.i18n
|
|
335
|
+
};
|
|
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),
|
|
285
346
|
// only when the template includes one, so a project without partials sends what it always sent
|
|
286
347
|
...Object.keys(tpl.partials).length ? { partials: tpl.partials } : {}
|
|
287
348
|
};
|
|
288
349
|
}
|
|
289
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
|
+
}
|
|
290
356
|
const payload = versionPayload(tpl);
|
|
291
357
|
const parts = [payload.html, payload.css, payload.head, payload.settings, payload.sample_data, payload.data_sets, payload.data_schema, payload.i18n];
|
|
292
358
|
if (payload.partials) parts.push(payload.partials);
|
|
@@ -307,6 +373,7 @@ function recordSync(project, slug, version, tpl) {
|
|
|
307
373
|
number: version.number,
|
|
308
374
|
status: version.status,
|
|
309
375
|
contentHash: contentHash(tpl),
|
|
376
|
+
...tpl.file ? { fileSha256: tpl.file.sha256 } : {},
|
|
310
377
|
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
311
378
|
};
|
|
312
379
|
writeState(project, state);
|
|
@@ -315,24 +382,85 @@ function titleFromSlug(slug) {
|
|
|
315
382
|
return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
316
383
|
}
|
|
317
384
|
function partialResolver(project) {
|
|
385
|
+
let shared = null;
|
|
318
386
|
return (name) => {
|
|
319
387
|
for (const candidate of [name, `${name}.html`]) {
|
|
320
388
|
const path = (0, import_node_path2.join)(project.partialsDir, candidate);
|
|
321
389
|
if ((0, import_node_fs2.existsSync)(path) && (0, import_node_fs2.statSync)(path).isFile()) return (0, import_node_fs2.readFileSync)(path, "utf8");
|
|
322
390
|
}
|
|
323
|
-
|
|
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"])
|
|
324
436
|
};
|
|
325
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
|
+
}
|
|
326
453
|
|
|
327
454
|
// packages/devkit/src/lib/local-render.ts
|
|
328
|
-
var
|
|
455
|
+
var import_engine4 = require("@formfeed/engine");
|
|
329
456
|
function diagnose(tpl, sampleData) {
|
|
330
|
-
|
|
457
|
+
if (tpl.file) return diagnoseOffice(tpl, tpl.file.bytes, sampleData);
|
|
458
|
+
const engine = (0, import_engine4.getEngine)(tpl.meta.engine);
|
|
331
459
|
const diagnostics = [];
|
|
332
460
|
try {
|
|
333
461
|
engine.compile(tpl.html, { name: tpl.slug });
|
|
334
462
|
} catch (e) {
|
|
335
|
-
if (e instanceof
|
|
463
|
+
if (e instanceof import_engine4.EngineSyntaxError) {
|
|
336
464
|
diagnostics.push({
|
|
337
465
|
severity: "error",
|
|
338
466
|
code: "syntax-error",
|
|
@@ -358,45 +486,105 @@ function diagnose(tpl, sampleData) {
|
|
|
358
486
|
});
|
|
359
487
|
return diagnostics;
|
|
360
488
|
}
|
|
361
|
-
|
|
362
|
-
|
|
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);
|
|
363
543
|
return {
|
|
364
544
|
locale: settings.locale ?? "en",
|
|
365
545
|
timezone: settings.timezone ?? "UTC",
|
|
366
546
|
currency: settings.currency ?? "EUR",
|
|
367
547
|
partials: partialResolver(project),
|
|
368
|
-
helpers: (0,
|
|
369
|
-
limits:
|
|
548
|
+
helpers: (0, import_engine4.defaultHelpers)(),
|
|
549
|
+
limits: import_engine4.defaultLimits,
|
|
370
550
|
i18n: tpl.i18n ?? void 0,
|
|
371
|
-
assetBaseUrl
|
|
551
|
+
assetBaseUrl,
|
|
552
|
+
// the organisation's kit from `formfeed brand pull`, else the empty kit the server uses too
|
|
553
|
+
brand: brand ?? readBrand(project)
|
|
372
554
|
};
|
|
373
555
|
}
|
|
374
556
|
async function renderLocal(project, tpl, data, options) {
|
|
375
|
-
const
|
|
557
|
+
const kind = htmlKind(tpl);
|
|
558
|
+
const ctx = renderContext(project, tpl, options.assetBaseUrl, options.brand);
|
|
376
559
|
if (options.locale) ctx.locale = options.locale;
|
|
377
|
-
return (0,
|
|
378
|
-
{ 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 },
|
|
379
562
|
data,
|
|
380
563
|
ctx,
|
|
381
564
|
options.mode,
|
|
382
565
|
options.vendor ? { vendor: options.vendor } : {}
|
|
383
566
|
);
|
|
384
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
|
+
}
|
|
385
572
|
function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
|
|
573
|
+
const kind = htmlKind(tpl);
|
|
386
574
|
const draft = {
|
|
387
575
|
document: rendered.document,
|
|
388
576
|
headerHtml: rendered.headerHtml,
|
|
389
577
|
footerHtml: rendered.footerHtml,
|
|
390
578
|
settings: rendered.settings,
|
|
391
|
-
kind
|
|
579
|
+
kind
|
|
392
580
|
};
|
|
393
|
-
return mode === "paged" &&
|
|
581
|
+
return mode === "paged" && kind === "pdf" ? (0, import_engine4.pagedDocument)(draft, { pagedScriptUrl }) : (0, import_engine4.flowDocument)(draft);
|
|
394
582
|
}
|
|
395
583
|
|
|
396
584
|
// packages/devkit/src/lib/files.ts
|
|
397
585
|
var import_node_crypto2 = require("node:crypto");
|
|
398
|
-
var
|
|
399
|
-
var
|
|
586
|
+
var import_node_fs4 = require("node:fs");
|
|
587
|
+
var import_node_path4 = require("node:path");
|
|
400
588
|
|
|
401
589
|
// packages/api-types/src/lib/assets.ts
|
|
402
590
|
var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
|
|
@@ -428,70 +616,644 @@ function sha256File(bytes) {
|
|
|
428
616
|
}
|
|
429
617
|
function listLocalFiles(project) {
|
|
430
618
|
const listing = { files: [], skipped: [] };
|
|
431
|
-
if (!(0,
|
|
432
|
-
const
|
|
433
|
-
for (const entry of (0,
|
|
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 })) {
|
|
434
622
|
if (entry.name.startsWith(".")) continue;
|
|
435
|
-
const path = (0,
|
|
623
|
+
const path = (0, import_node_path4.join)(dir, entry.name);
|
|
436
624
|
if (entry.isDirectory()) {
|
|
437
|
-
|
|
625
|
+
walk2(path);
|
|
438
626
|
continue;
|
|
439
627
|
}
|
|
440
628
|
if (!entry.isFile()) continue;
|
|
441
|
-
const name = (0,
|
|
442
|
-
const size = (0,
|
|
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;
|
|
443
631
|
const contentType = contentTypeFor(name);
|
|
444
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;
|
|
445
633
|
if (reason) {
|
|
446
634
|
listing.skipped.push({ path, reason });
|
|
447
635
|
continue;
|
|
448
636
|
}
|
|
449
|
-
const bytes = (0,
|
|
637
|
+
const bytes = (0, import_node_fs4.readFileSync)(path);
|
|
450
638
|
listing.files.push({ name, path, bytes: size, sha256: sha256File(bytes), contentType });
|
|
451
639
|
}
|
|
452
640
|
};
|
|
453
|
-
|
|
641
|
+
walk2(project.filesDir);
|
|
454
642
|
listing.files.sort((a, b) => a.name.localeCompare(b.name));
|
|
455
643
|
return listing;
|
|
456
644
|
}
|
|
457
645
|
function localFilePath(project, name) {
|
|
458
646
|
if (!isAssetName(name)) throw new DevkitError(`"${name}" is not a library file name`);
|
|
459
|
-
const path = (0,
|
|
460
|
-
if (!path.startsWith((0,
|
|
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`);
|
|
461
649
|
return path;
|
|
462
650
|
}
|
|
463
651
|
function writeLocalFile(project, name, bytes) {
|
|
464
652
|
const path = localFilePath(project, name);
|
|
465
|
-
(0,
|
|
466
|
-
(0,
|
|
653
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
654
|
+
(0, import_node_fs4.writeFileSync)(path, bytes);
|
|
467
655
|
return path;
|
|
468
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);
|
|
1211
|
+
}
|
|
469
1212
|
// Annotate the CommonJS export names for ESM import in node:
|
|
470
1213
|
0 && (module.exports = {
|
|
471
1214
|
DevkitError,
|
|
1215
|
+
brandFromJson,
|
|
1216
|
+
brandPath,
|
|
472
1217
|
collectPartials,
|
|
473
1218
|
contentHash,
|
|
474
1219
|
contentTypeFor,
|
|
475
1220
|
defaultData,
|
|
476
1221
|
defaultProjectConfig,
|
|
1222
|
+
defaultTypesFile,
|
|
477
1223
|
diagnose,
|
|
478
1224
|
findProject,
|
|
1225
|
+
generateTypes,
|
|
479
1226
|
listLocalFiles,
|
|
480
1227
|
listTemplateSlugs,
|
|
481
1228
|
loadProject,
|
|
482
1229
|
localFilePath,
|
|
1230
|
+
localTypeSource,
|
|
1231
|
+
officeSnapshot,
|
|
1232
|
+
officeVersionPayload,
|
|
1233
|
+
parseRedactPath,
|
|
1234
|
+
partialContentHash,
|
|
483
1235
|
partialResolver,
|
|
1236
|
+
prettyXml,
|
|
484
1237
|
previewDocument,
|
|
485
1238
|
projectAround,
|
|
1239
|
+
readBrand,
|
|
486
1240
|
readState,
|
|
487
1241
|
readTemplate,
|
|
1242
|
+
recordSharedPartial,
|
|
488
1243
|
recordSync,
|
|
1244
|
+
redactData,
|
|
1245
|
+
redactString,
|
|
489
1246
|
renderContext,
|
|
490
1247
|
renderLocal,
|
|
1248
|
+
renderOfficeLocal,
|
|
491
1249
|
sha256File,
|
|
1250
|
+
sharedPartialNames,
|
|
1251
|
+
sharedPartialPath,
|
|
492
1252
|
templateDir,
|
|
1253
|
+
templateFileName,
|
|
493
1254
|
titleFromSlug,
|
|
494
1255
|
versionPayload,
|
|
1256
|
+
writeBrand,
|
|
495
1257
|
writeLocalFile,
|
|
496
1258
|
writePartials,
|
|
497
1259
|
writeProjectConfig,
|