@formfeed/devkit 0.1.0 → 0.2.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 +177 -18
- package/index.js +164 -12
- package/package.json +3 -3
- package/types/index.d.cts +5 -1
- package/types/index.d.ts +5 -1
- package/types/lib/files.d.cts +32 -0
- package/types/lib/files.d.ts +32 -0
- package/types/lib/local-render.d.cts +8 -1
- package/types/lib/local-render.d.ts +8 -1
- package/types/lib/partials.d.cts +17 -0
- package/types/lib/partials.d.ts +17 -0
- package/types/lib/project-config.d.cts +3 -0
- package/types/lib/project-config.d.ts +3 -0
- package/types/lib/project.d.cts +12 -0
- package/types/lib/project.d.ts +12 -0
package/index.cjs
CHANGED
|
@@ -21,13 +21,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
DevkitError: () => DevkitError,
|
|
24
|
+
collectPartials: () => collectPartials,
|
|
24
25
|
contentHash: () => contentHash,
|
|
26
|
+
contentTypeFor: () => contentTypeFor,
|
|
25
27
|
defaultData: () => defaultData,
|
|
26
28
|
defaultProjectConfig: () => defaultProjectConfig,
|
|
27
29
|
diagnose: () => diagnose,
|
|
28
30
|
findProject: () => findProject,
|
|
31
|
+
listLocalFiles: () => listLocalFiles,
|
|
29
32
|
listTemplateSlugs: () => listTemplateSlugs,
|
|
30
33
|
loadProject: () => loadProject,
|
|
34
|
+
localFilePath: () => localFilePath,
|
|
31
35
|
partialResolver: () => partialResolver,
|
|
32
36
|
previewDocument: () => previewDocument,
|
|
33
37
|
projectAround: () => projectAround,
|
|
@@ -36,9 +40,12 @@ __export(index_exports, {
|
|
|
36
40
|
recordSync: () => recordSync,
|
|
37
41
|
renderContext: () => renderContext,
|
|
38
42
|
renderLocal: () => renderLocal,
|
|
43
|
+
sha256File: () => sha256File,
|
|
39
44
|
templateDir: () => templateDir,
|
|
40
45
|
titleFromSlug: () => titleFromSlug,
|
|
41
46
|
versionPayload: () => versionPayload,
|
|
47
|
+
writeLocalFile: () => writeLocalFile,
|
|
48
|
+
writePartials: () => writePartials,
|
|
42
49
|
writeProjectConfig: () => writeProjectConfig,
|
|
43
50
|
writeState: () => writeState,
|
|
44
51
|
writeTemplate: () => writeTemplate
|
|
@@ -60,6 +67,7 @@ var import_node_path = require("node:path");
|
|
|
60
67
|
var defaultProjectConfig = {
|
|
61
68
|
templatesDir: "templates",
|
|
62
69
|
partialsDir: "partials",
|
|
70
|
+
filesDir: "files",
|
|
63
71
|
engine: "jinja2",
|
|
64
72
|
ignore: ["**/drafts/**"]
|
|
65
73
|
};
|
|
@@ -82,7 +90,14 @@ function loadProject(configPath) {
|
|
|
82
90
|
}
|
|
83
91
|
const config = { ...defaultProjectConfig, ...raw };
|
|
84
92
|
const root = (0, import_node_path.dirname)(configPath);
|
|
85
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
root,
|
|
95
|
+
configPath,
|
|
96
|
+
config,
|
|
97
|
+
templatesDir: (0, import_node_path.resolve)(root, config.templatesDir),
|
|
98
|
+
partialsDir: (0, import_node_path.resolve)(root, config.partialsDir),
|
|
99
|
+
filesDir: (0, import_node_path.resolve)(root, config.filesDir)
|
|
100
|
+
};
|
|
86
101
|
}
|
|
87
102
|
function projectAround(templateFolder) {
|
|
88
103
|
const dir = (0, import_node_path.resolve)(templateFolder);
|
|
@@ -93,9 +108,10 @@ function projectAround(templateFolder) {
|
|
|
93
108
|
return {
|
|
94
109
|
root,
|
|
95
110
|
configPath: null,
|
|
96
|
-
config: { ...defaultProjectConfig, templatesDir, partialsDir: (0, import_node_path.join)(root, "partials") },
|
|
111
|
+
config: { ...defaultProjectConfig, templatesDir, partialsDir: (0, import_node_path.join)(root, "partials"), filesDir: (0, import_node_path.join)(root, "files") },
|
|
97
112
|
templatesDir,
|
|
98
|
-
partialsDir: (0, import_node_path.join)(root, "partials")
|
|
113
|
+
partialsDir: (0, import_node_path.join)(root, "partials"),
|
|
114
|
+
filesDir: (0, import_node_path.join)(root, "files")
|
|
99
115
|
};
|
|
100
116
|
}
|
|
101
117
|
function writeProjectConfig(root, config) {
|
|
@@ -109,6 +125,39 @@ function writeProjectConfig(root, config) {
|
|
|
109
125
|
var import_node_crypto = require("node:crypto");
|
|
110
126
|
var import_node_fs2 = require("node:fs");
|
|
111
127
|
var import_node_path2 = require("node:path");
|
|
128
|
+
|
|
129
|
+
// packages/devkit/src/lib/partials.ts
|
|
130
|
+
var import_engine = require("@formfeed/engine");
|
|
131
|
+
function includeNames(engine, source) {
|
|
132
|
+
try {
|
|
133
|
+
return (0, import_engine.getEngine)(engine).analyze(source).includes.map((include) => include.name);
|
|
134
|
+
} catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function collectPartials(project, engine, sources) {
|
|
139
|
+
const resolve3 = partialResolver(project);
|
|
140
|
+
const partials = {};
|
|
141
|
+
const missing = /* @__PURE__ */ new Set();
|
|
142
|
+
const queue = sources.filter((source) => Boolean(source));
|
|
143
|
+
const seen = /* @__PURE__ */ new Set();
|
|
144
|
+
while (queue.length) {
|
|
145
|
+
for (const name of includeNames(engine, queue.shift())) {
|
|
146
|
+
if (seen.has(name)) continue;
|
|
147
|
+
seen.add(name);
|
|
148
|
+
const source = resolve3(name);
|
|
149
|
+
if (source === void 0) {
|
|
150
|
+
missing.add(name);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
partials[name] = source;
|
|
154
|
+
queue.push(source);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { partials, missing: [...missing].sort() };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// packages/devkit/src/lib/project.ts
|
|
112
161
|
var readText = (path) => (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : null;
|
|
113
162
|
function readJson(path, what) {
|
|
114
163
|
const text = readText(path);
|
|
@@ -150,6 +199,7 @@ function readTemplate(project, slug) {
|
|
|
150
199
|
dataSets[(0, import_node_path2.basename)(file, ".json")] = readJson((0, import_node_path2.join)(dataDir, file), `data/${file}`);
|
|
151
200
|
}
|
|
152
201
|
}
|
|
202
|
+
const { partials, missing } = collectPartials(project, meta.engine, [html, settings.header?.html, settings.footer?.html]);
|
|
153
203
|
return {
|
|
154
204
|
slug,
|
|
155
205
|
dir,
|
|
@@ -160,7 +210,9 @@ function readTemplate(project, slug) {
|
|
|
160
210
|
settings,
|
|
161
211
|
dataSets,
|
|
162
212
|
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")
|
|
213
|
+
i18n: readJson((0, import_node_path2.join)(dir, "i18n.json"), "i18n.json"),
|
|
214
|
+
partials,
|
|
215
|
+
missingPartials: missing
|
|
164
216
|
};
|
|
165
217
|
}
|
|
166
218
|
function defaultData(tpl, name) {
|
|
@@ -191,8 +243,22 @@ function writeTemplate(project, slug, meta, version) {
|
|
|
191
243
|
(0, import_node_fs2.writeFileSync)((0, import_node_path2.join)(dir, "data", `${name}.json`), JSON.stringify(data ?? {}, null, 2) + "\n");
|
|
192
244
|
writeOrRemove((0, import_node_path2.join)(dir, "schema.json"), version.data_schema ? JSON.stringify(version.data_schema, null, 2) + "\n" : "");
|
|
193
245
|
writeOrRemove((0, import_node_path2.join)(dir, "i18n.json"), version.i18n ? JSON.stringify(version.i18n, null, 2) + "\n" : "");
|
|
246
|
+
writePartials(project, version.partials ?? null);
|
|
194
247
|
return dir;
|
|
195
248
|
}
|
|
249
|
+
var partialName = /^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/;
|
|
250
|
+
function writePartials(project, partials) {
|
|
251
|
+
const changed = [];
|
|
252
|
+
for (const [name, source] of Object.entries(partials ?? {})) {
|
|
253
|
+
if (!partialName.test(name)) continue;
|
|
254
|
+
const path = (0, import_node_path2.join)(project.partialsDir, name.includes(".") ? name : `${name}.html`);
|
|
255
|
+
if (readText(path) === source) continue;
|
|
256
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path), { recursive: true });
|
|
257
|
+
(0, import_node_fs2.writeFileSync)(path, source);
|
|
258
|
+
changed.push(path);
|
|
259
|
+
}
|
|
260
|
+
return changed;
|
|
261
|
+
}
|
|
196
262
|
function stripHtml(part) {
|
|
197
263
|
const { html: _html, ...rest } = part;
|
|
198
264
|
void _html;
|
|
@@ -215,12 +281,16 @@ function versionPayload(tpl) {
|
|
|
215
281
|
sample_data: sample ?? {},
|
|
216
282
|
data_sets: named,
|
|
217
283
|
data_schema: tpl.dataSchema,
|
|
218
|
-
i18n: tpl.i18n
|
|
284
|
+
i18n: tpl.i18n,
|
|
285
|
+
// only when the template includes one, so a project without partials sends what it always sent
|
|
286
|
+
...Object.keys(tpl.partials).length ? { partials: tpl.partials } : {}
|
|
219
287
|
};
|
|
220
288
|
}
|
|
221
289
|
function contentHash(tpl) {
|
|
222
290
|
const payload = versionPayload(tpl);
|
|
223
|
-
|
|
291
|
+
const parts = [payload.html, payload.css, payload.head, payload.settings, payload.sample_data, payload.data_sets, payload.data_schema, payload.i18n];
|
|
292
|
+
if (payload.partials) parts.push(payload.partials);
|
|
293
|
+
return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
224
294
|
}
|
|
225
295
|
var statePath = (project) => (0, import_node_path2.join)(project.root, ".formfeed", "state.json");
|
|
226
296
|
function readState(project) {
|
|
@@ -255,14 +325,14 @@ function partialResolver(project) {
|
|
|
255
325
|
}
|
|
256
326
|
|
|
257
327
|
// packages/devkit/src/lib/local-render.ts
|
|
258
|
-
var
|
|
328
|
+
var import_engine2 = require("@formfeed/engine");
|
|
259
329
|
function diagnose(tpl, sampleData) {
|
|
260
|
-
const engine = (0,
|
|
330
|
+
const engine = (0, import_engine2.getEngine)(tpl.meta.engine);
|
|
261
331
|
const diagnostics = [];
|
|
262
332
|
try {
|
|
263
333
|
engine.compile(tpl.html, { name: tpl.slug });
|
|
264
334
|
} catch (e) {
|
|
265
|
-
if (e instanceof
|
|
335
|
+
if (e instanceof import_engine2.EngineSyntaxError) {
|
|
266
336
|
diagnostics.push({
|
|
267
337
|
severity: "error",
|
|
268
338
|
code: "syntax-error",
|
|
@@ -279,25 +349,33 @@ function diagnose(tpl, sampleData) {
|
|
|
279
349
|
if (!source) continue;
|
|
280
350
|
for (const d of engine.analyze(source, { sampleData }).diagnostics) diagnostics.push({ ...d, message: `${part}: ${d.message}` });
|
|
281
351
|
}
|
|
352
|
+
for (const name of tpl.missingPartials)
|
|
353
|
+
diagnostics.push({
|
|
354
|
+
severity: "error",
|
|
355
|
+
code: "missing-partial",
|
|
356
|
+
message: `No partial "${name}" in the project's partials folder`,
|
|
357
|
+
range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }
|
|
358
|
+
});
|
|
282
359
|
return diagnostics;
|
|
283
360
|
}
|
|
284
|
-
function renderContext(project, tpl) {
|
|
285
|
-
const settings = (0,
|
|
361
|
+
function renderContext(project, tpl, assetBaseUrl) {
|
|
362
|
+
const settings = (0, import_engine2.mergeSettings)(tpl.settings);
|
|
286
363
|
return {
|
|
287
364
|
locale: settings.locale ?? "en",
|
|
288
365
|
timezone: settings.timezone ?? "UTC",
|
|
289
366
|
currency: settings.currency ?? "EUR",
|
|
290
367
|
partials: partialResolver(project),
|
|
291
|
-
helpers: (0,
|
|
292
|
-
limits:
|
|
293
|
-
i18n: tpl.i18n ?? void 0
|
|
368
|
+
helpers: (0, import_engine2.defaultHelpers)(),
|
|
369
|
+
limits: import_engine2.defaultLimits,
|
|
370
|
+
i18n: tpl.i18n ?? void 0,
|
|
371
|
+
assetBaseUrl
|
|
294
372
|
};
|
|
295
373
|
}
|
|
296
374
|
async function renderLocal(project, tpl, data, options) {
|
|
297
|
-
const ctx = renderContext(project, tpl);
|
|
375
|
+
const ctx = renderContext(project, tpl, options.assetBaseUrl);
|
|
298
376
|
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,
|
|
377
|
+
return (0, import_engine2.renderVersion)(
|
|
378
|
+
{ engine: tpl.meta.engine, html: tpl.html, css: tpl.css, head: tpl.head, settings: (0, import_engine2.mergeSettings)(tpl.settings), kind: tpl.meta.kind },
|
|
301
379
|
data,
|
|
302
380
|
ctx,
|
|
303
381
|
options.mode,
|
|
@@ -312,18 +390,96 @@ function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
|
|
|
312
390
|
settings: rendered.settings,
|
|
313
391
|
kind: tpl.meta.kind
|
|
314
392
|
};
|
|
315
|
-
return mode === "paged" && tpl.meta.kind === "pdf" ? (0,
|
|
393
|
+
return mode === "paged" && tpl.meta.kind === "pdf" ? (0, import_engine2.pagedDocument)(draft, { pagedScriptUrl }) : (0, import_engine2.flowDocument)(draft);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// packages/devkit/src/lib/files.ts
|
|
397
|
+
var import_node_crypto2 = require("node:crypto");
|
|
398
|
+
var import_node_fs3 = require("node:fs");
|
|
399
|
+
var import_node_path3 = require("node:path");
|
|
400
|
+
|
|
401
|
+
// packages/api-types/src/lib/assets.ts
|
|
402
|
+
var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
|
|
403
|
+
var ASSET_NAME_PATTERN = new RegExp(`^${SEGMENT}(?:/${SEGMENT})*$`);
|
|
404
|
+
var ASSET_MAX_BYTES = 10 * 1024 * 1024;
|
|
405
|
+
var ASSET_CONTENT_TYPES = {
|
|
406
|
+
"image/png": "png",
|
|
407
|
+
"image/jpeg": "jpg",
|
|
408
|
+
"image/webp": "webp",
|
|
409
|
+
"image/gif": "gif",
|
|
410
|
+
"image/svg+xml": "svg",
|
|
411
|
+
"image/avif": "avif",
|
|
412
|
+
"application/pdf": "pdf"
|
|
413
|
+
};
|
|
414
|
+
function isAssetName(name) {
|
|
415
|
+
return name.length > 0 && name.length <= 200 && ASSET_NAME_PATTERN.test(name) && !name.split("/").includes("..");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// packages/devkit/src/lib/files.ts
|
|
419
|
+
var byExtension = Object.fromEntries(
|
|
420
|
+
Object.entries(ASSET_CONTENT_TYPES).map(([type, ext]) => [ext, type])
|
|
421
|
+
);
|
|
422
|
+
byExtension["jpeg"] = "image/jpeg";
|
|
423
|
+
function contentTypeFor(name) {
|
|
424
|
+
return byExtension[name.split(".").pop()?.toLowerCase() ?? ""] ?? null;
|
|
425
|
+
}
|
|
426
|
+
function sha256File(bytes) {
|
|
427
|
+
return (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex");
|
|
428
|
+
}
|
|
429
|
+
function listLocalFiles(project) {
|
|
430
|
+
const listing = { files: [], skipped: [] };
|
|
431
|
+
if (!(0, import_node_fs3.existsSync)(project.filesDir)) return listing;
|
|
432
|
+
const walk = (dir) => {
|
|
433
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
434
|
+
if (entry.name.startsWith(".")) continue;
|
|
435
|
+
const path = (0, import_node_path3.join)(dir, entry.name);
|
|
436
|
+
if (entry.isDirectory()) {
|
|
437
|
+
walk(path);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (!entry.isFile()) continue;
|
|
441
|
+
const name = (0, import_node_path3.relative)(project.filesDir, path).split(import_node_path3.sep).join("/");
|
|
442
|
+
const size = (0, import_node_fs3.statSync)(path).size;
|
|
443
|
+
const contentType = contentTypeFor(name);
|
|
444
|
+
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
|
+
if (reason) {
|
|
446
|
+
listing.skipped.push({ path, reason });
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const bytes = (0, import_node_fs3.readFileSync)(path);
|
|
450
|
+
listing.files.push({ name, path, bytes: size, sha256: sha256File(bytes), contentType });
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
walk(project.filesDir);
|
|
454
|
+
listing.files.sort((a, b) => a.name.localeCompare(b.name));
|
|
455
|
+
return listing;
|
|
456
|
+
}
|
|
457
|
+
function localFilePath(project, name) {
|
|
458
|
+
if (!isAssetName(name)) throw new DevkitError(`"${name}" is not a library file name`);
|
|
459
|
+
const path = (0, import_node_path3.resolve)(project.filesDir, ...name.split("/"));
|
|
460
|
+
if (!path.startsWith((0, import_node_path3.resolve)(project.filesDir) + import_node_path3.sep)) throw new DevkitError(`"${name}" is not inside the files folder`);
|
|
461
|
+
return path;
|
|
462
|
+
}
|
|
463
|
+
function writeLocalFile(project, name, bytes) {
|
|
464
|
+
const path = localFilePath(project, name);
|
|
465
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(path), { recursive: true });
|
|
466
|
+
(0, import_node_fs3.writeFileSync)(path, bytes);
|
|
467
|
+
return path;
|
|
316
468
|
}
|
|
317
469
|
// Annotate the CommonJS export names for ESM import in node:
|
|
318
470
|
0 && (module.exports = {
|
|
319
471
|
DevkitError,
|
|
472
|
+
collectPartials,
|
|
320
473
|
contentHash,
|
|
474
|
+
contentTypeFor,
|
|
321
475
|
defaultData,
|
|
322
476
|
defaultProjectConfig,
|
|
323
477
|
diagnose,
|
|
324
478
|
findProject,
|
|
479
|
+
listLocalFiles,
|
|
325
480
|
listTemplateSlugs,
|
|
326
481
|
loadProject,
|
|
482
|
+
localFilePath,
|
|
327
483
|
partialResolver,
|
|
328
484
|
previewDocument,
|
|
329
485
|
projectAround,
|
|
@@ -332,9 +488,12 @@ function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
|
|
|
332
488
|
recordSync,
|
|
333
489
|
renderContext,
|
|
334
490
|
renderLocal,
|
|
491
|
+
sha256File,
|
|
335
492
|
templateDir,
|
|
336
493
|
titleFromSlug,
|
|
337
494
|
versionPayload,
|
|
495
|
+
writeLocalFile,
|
|
496
|
+
writePartials,
|
|
338
497
|
writeProjectConfig,
|
|
339
498
|
writeState,
|
|
340
499
|
writeTemplate
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
13
13
|
var defaultProjectConfig = {
|
|
14
14
|
templatesDir: "templates",
|
|
15
15
|
partialsDir: "partials",
|
|
16
|
+
filesDir: "files",
|
|
16
17
|
engine: "jinja2",
|
|
17
18
|
ignore: ["**/drafts/**"]
|
|
18
19
|
};
|
|
@@ -35,7 +36,14 @@ function loadProject(configPath) {
|
|
|
35
36
|
}
|
|
36
37
|
const config = { ...defaultProjectConfig, ...raw };
|
|
37
38
|
const root = dirname(configPath);
|
|
38
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
root,
|
|
41
|
+
configPath,
|
|
42
|
+
config,
|
|
43
|
+
templatesDir: resolve(root, config.templatesDir),
|
|
44
|
+
partialsDir: resolve(root, config.partialsDir),
|
|
45
|
+
filesDir: resolve(root, config.filesDir)
|
|
46
|
+
};
|
|
39
47
|
}
|
|
40
48
|
function projectAround(templateFolder) {
|
|
41
49
|
const dir = resolve(templateFolder);
|
|
@@ -46,9 +54,10 @@ function projectAround(templateFolder) {
|
|
|
46
54
|
return {
|
|
47
55
|
root,
|
|
48
56
|
configPath: null,
|
|
49
|
-
config: { ...defaultProjectConfig, templatesDir, partialsDir: join(root, "partials") },
|
|
57
|
+
config: { ...defaultProjectConfig, templatesDir, partialsDir: join(root, "partials"), filesDir: join(root, "files") },
|
|
50
58
|
templatesDir,
|
|
51
|
-
partialsDir: join(root, "partials")
|
|
59
|
+
partialsDir: join(root, "partials"),
|
|
60
|
+
filesDir: join(root, "files")
|
|
52
61
|
};
|
|
53
62
|
}
|
|
54
63
|
function writeProjectConfig(root, config) {
|
|
@@ -61,7 +70,40 @@ function writeProjectConfig(root, config) {
|
|
|
61
70
|
// packages/devkit/src/lib/project.ts
|
|
62
71
|
import { createHash } from "node:crypto";
|
|
63
72
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, readdirSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
64
|
-
import { basename, join as join2 } from "node:path";
|
|
73
|
+
import { basename, dirname as dirname2, join as join2 } from "node:path";
|
|
74
|
+
|
|
75
|
+
// packages/devkit/src/lib/partials.ts
|
|
76
|
+
import { getEngine } from "@formfeed/engine";
|
|
77
|
+
function includeNames(engine, source) {
|
|
78
|
+
try {
|
|
79
|
+
return getEngine(engine).analyze(source).includes.map((include) => include.name);
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function collectPartials(project, engine, sources) {
|
|
85
|
+
const resolve3 = partialResolver(project);
|
|
86
|
+
const partials = {};
|
|
87
|
+
const missing = /* @__PURE__ */ new Set();
|
|
88
|
+
const queue = sources.filter((source) => Boolean(source));
|
|
89
|
+
const seen = /* @__PURE__ */ new Set();
|
|
90
|
+
while (queue.length) {
|
|
91
|
+
for (const name of includeNames(engine, queue.shift())) {
|
|
92
|
+
if (seen.has(name)) continue;
|
|
93
|
+
seen.add(name);
|
|
94
|
+
const source = resolve3(name);
|
|
95
|
+
if (source === void 0) {
|
|
96
|
+
missing.add(name);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
partials[name] = source;
|
|
100
|
+
queue.push(source);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { partials, missing: [...missing].sort() };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// packages/devkit/src/lib/project.ts
|
|
65
107
|
var readText = (path) => existsSync2(path) ? readFileSync2(path, "utf8") : null;
|
|
66
108
|
function readJson(path, what) {
|
|
67
109
|
const text = readText(path);
|
|
@@ -103,6 +145,7 @@ function readTemplate(project, slug) {
|
|
|
103
145
|
dataSets[basename(file, ".json")] = readJson(join2(dataDir, file), `data/${file}`);
|
|
104
146
|
}
|
|
105
147
|
}
|
|
148
|
+
const { partials, missing } = collectPartials(project, meta.engine, [html, settings.header?.html, settings.footer?.html]);
|
|
106
149
|
return {
|
|
107
150
|
slug,
|
|
108
151
|
dir,
|
|
@@ -113,7 +156,9 @@ function readTemplate(project, slug) {
|
|
|
113
156
|
settings,
|
|
114
157
|
dataSets,
|
|
115
158
|
dataSchema: readJson(join2(dir, "schema.json"), "schema.json"),
|
|
116
|
-
i18n: readJson(join2(dir, "i18n.json"), "i18n.json")
|
|
159
|
+
i18n: readJson(join2(dir, "i18n.json"), "i18n.json"),
|
|
160
|
+
partials,
|
|
161
|
+
missingPartials: missing
|
|
117
162
|
};
|
|
118
163
|
}
|
|
119
164
|
function defaultData(tpl, name) {
|
|
@@ -144,8 +189,22 @@ function writeTemplate(project, slug, meta, version) {
|
|
|
144
189
|
writeFileSync2(join2(dir, "data", `${name}.json`), JSON.stringify(data ?? {}, null, 2) + "\n");
|
|
145
190
|
writeOrRemove(join2(dir, "schema.json"), version.data_schema ? JSON.stringify(version.data_schema, null, 2) + "\n" : "");
|
|
146
191
|
writeOrRemove(join2(dir, "i18n.json"), version.i18n ? JSON.stringify(version.i18n, null, 2) + "\n" : "");
|
|
192
|
+
writePartials(project, version.partials ?? null);
|
|
147
193
|
return dir;
|
|
148
194
|
}
|
|
195
|
+
var partialName = /^[A-Za-z0-9_-][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/;
|
|
196
|
+
function writePartials(project, partials) {
|
|
197
|
+
const changed = [];
|
|
198
|
+
for (const [name, source] of Object.entries(partials ?? {})) {
|
|
199
|
+
if (!partialName.test(name)) continue;
|
|
200
|
+
const path = join2(project.partialsDir, name.includes(".") ? name : `${name}.html`);
|
|
201
|
+
if (readText(path) === source) continue;
|
|
202
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
203
|
+
writeFileSync2(path, source);
|
|
204
|
+
changed.push(path);
|
|
205
|
+
}
|
|
206
|
+
return changed;
|
|
207
|
+
}
|
|
149
208
|
function stripHtml(part) {
|
|
150
209
|
const { html: _html, ...rest } = part;
|
|
151
210
|
void _html;
|
|
@@ -168,12 +227,16 @@ function versionPayload(tpl) {
|
|
|
168
227
|
sample_data: sample ?? {},
|
|
169
228
|
data_sets: named,
|
|
170
229
|
data_schema: tpl.dataSchema,
|
|
171
|
-
i18n: tpl.i18n
|
|
230
|
+
i18n: tpl.i18n,
|
|
231
|
+
// only when the template includes one, so a project without partials sends what it always sent
|
|
232
|
+
...Object.keys(tpl.partials).length ? { partials: tpl.partials } : {}
|
|
172
233
|
};
|
|
173
234
|
}
|
|
174
235
|
function contentHash(tpl) {
|
|
175
236
|
const payload = versionPayload(tpl);
|
|
176
|
-
|
|
237
|
+
const parts = [payload.html, payload.css, payload.head, payload.settings, payload.sample_data, payload.data_sets, payload.data_schema, payload.i18n];
|
|
238
|
+
if (payload.partials) parts.push(payload.partials);
|
|
239
|
+
return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
|
|
177
240
|
}
|
|
178
241
|
var statePath = (project) => join2(project.root, ".formfeed", "state.json");
|
|
179
242
|
function readState(project) {
|
|
@@ -213,13 +276,13 @@ import {
|
|
|
213
276
|
defaultHelpers,
|
|
214
277
|
defaultLimits,
|
|
215
278
|
flowDocument,
|
|
216
|
-
getEngine,
|
|
279
|
+
getEngine as getEngine2,
|
|
217
280
|
mergeSettings,
|
|
218
281
|
pagedDocument,
|
|
219
282
|
renderVersion
|
|
220
283
|
} from "@formfeed/engine";
|
|
221
284
|
function diagnose(tpl, sampleData) {
|
|
222
|
-
const engine =
|
|
285
|
+
const engine = getEngine2(tpl.meta.engine);
|
|
223
286
|
const diagnostics = [];
|
|
224
287
|
try {
|
|
225
288
|
engine.compile(tpl.html, { name: tpl.slug });
|
|
@@ -241,9 +304,16 @@ function diagnose(tpl, sampleData) {
|
|
|
241
304
|
if (!source) continue;
|
|
242
305
|
for (const d of engine.analyze(source, { sampleData }).diagnostics) diagnostics.push({ ...d, message: `${part}: ${d.message}` });
|
|
243
306
|
}
|
|
307
|
+
for (const name of tpl.missingPartials)
|
|
308
|
+
diagnostics.push({
|
|
309
|
+
severity: "error",
|
|
310
|
+
code: "missing-partial",
|
|
311
|
+
message: `No partial "${name}" in the project's partials folder`,
|
|
312
|
+
range: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } }
|
|
313
|
+
});
|
|
244
314
|
return diagnostics;
|
|
245
315
|
}
|
|
246
|
-
function renderContext(project, tpl) {
|
|
316
|
+
function renderContext(project, tpl, assetBaseUrl) {
|
|
247
317
|
const settings = mergeSettings(tpl.settings);
|
|
248
318
|
return {
|
|
249
319
|
locale: settings.locale ?? "en",
|
|
@@ -252,11 +322,12 @@ function renderContext(project, tpl) {
|
|
|
252
322
|
partials: partialResolver(project),
|
|
253
323
|
helpers: defaultHelpers(),
|
|
254
324
|
limits: defaultLimits,
|
|
255
|
-
i18n: tpl.i18n ?? void 0
|
|
325
|
+
i18n: tpl.i18n ?? void 0,
|
|
326
|
+
assetBaseUrl
|
|
256
327
|
};
|
|
257
328
|
}
|
|
258
329
|
async function renderLocal(project, tpl, data, options) {
|
|
259
|
-
const ctx = renderContext(project, tpl);
|
|
330
|
+
const ctx = renderContext(project, tpl, options.assetBaseUrl);
|
|
260
331
|
if (options.locale) ctx.locale = options.locale;
|
|
261
332
|
return renderVersion(
|
|
262
333
|
{ engine: tpl.meta.engine, html: tpl.html, css: tpl.css, head: tpl.head, settings: mergeSettings(tpl.settings), kind: tpl.meta.kind },
|
|
@@ -276,15 +347,93 @@ function previewDocument(tpl, rendered, mode, pagedScriptUrl) {
|
|
|
276
347
|
};
|
|
277
348
|
return mode === "paged" && tpl.meta.kind === "pdf" ? pagedDocument(draft, { pagedScriptUrl }) : flowDocument(draft);
|
|
278
349
|
}
|
|
350
|
+
|
|
351
|
+
// packages/devkit/src/lib/files.ts
|
|
352
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
353
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
354
|
+
import { dirname as dirname3, join as join3, relative, resolve as resolve2, sep } from "node:path";
|
|
355
|
+
|
|
356
|
+
// packages/api-types/src/lib/assets.ts
|
|
357
|
+
var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
|
|
358
|
+
var ASSET_NAME_PATTERN = new RegExp(`^${SEGMENT}(?:/${SEGMENT})*$`);
|
|
359
|
+
var ASSET_MAX_BYTES = 10 * 1024 * 1024;
|
|
360
|
+
var ASSET_CONTENT_TYPES = {
|
|
361
|
+
"image/png": "png",
|
|
362
|
+
"image/jpeg": "jpg",
|
|
363
|
+
"image/webp": "webp",
|
|
364
|
+
"image/gif": "gif",
|
|
365
|
+
"image/svg+xml": "svg",
|
|
366
|
+
"image/avif": "avif",
|
|
367
|
+
"application/pdf": "pdf"
|
|
368
|
+
};
|
|
369
|
+
function isAssetName(name) {
|
|
370
|
+
return name.length > 0 && name.length <= 200 && ASSET_NAME_PATTERN.test(name) && !name.split("/").includes("..");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// packages/devkit/src/lib/files.ts
|
|
374
|
+
var byExtension = Object.fromEntries(
|
|
375
|
+
Object.entries(ASSET_CONTENT_TYPES).map(([type, ext]) => [ext, type])
|
|
376
|
+
);
|
|
377
|
+
byExtension["jpeg"] = "image/jpeg";
|
|
378
|
+
function contentTypeFor(name) {
|
|
379
|
+
return byExtension[name.split(".").pop()?.toLowerCase() ?? ""] ?? null;
|
|
380
|
+
}
|
|
381
|
+
function sha256File(bytes) {
|
|
382
|
+
return createHash2("sha256").update(bytes).digest("hex");
|
|
383
|
+
}
|
|
384
|
+
function listLocalFiles(project) {
|
|
385
|
+
const listing = { files: [], skipped: [] };
|
|
386
|
+
if (!existsSync3(project.filesDir)) return listing;
|
|
387
|
+
const walk = (dir) => {
|
|
388
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
389
|
+
if (entry.name.startsWith(".")) continue;
|
|
390
|
+
const path = join3(dir, entry.name);
|
|
391
|
+
if (entry.isDirectory()) {
|
|
392
|
+
walk(path);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (!entry.isFile()) continue;
|
|
396
|
+
const name = relative(project.filesDir, path).split(sep).join("/");
|
|
397
|
+
const size = statSync2(path).size;
|
|
398
|
+
const contentType = contentTypeFor(name);
|
|
399
|
+
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;
|
|
400
|
+
if (reason) {
|
|
401
|
+
listing.skipped.push({ path, reason });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const bytes = readFileSync3(path);
|
|
405
|
+
listing.files.push({ name, path, bytes: size, sha256: sha256File(bytes), contentType });
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
walk(project.filesDir);
|
|
409
|
+
listing.files.sort((a, b) => a.name.localeCompare(b.name));
|
|
410
|
+
return listing;
|
|
411
|
+
}
|
|
412
|
+
function localFilePath(project, name) {
|
|
413
|
+
if (!isAssetName(name)) throw new DevkitError(`"${name}" is not a library file name`);
|
|
414
|
+
const path = resolve2(project.filesDir, ...name.split("/"));
|
|
415
|
+
if (!path.startsWith(resolve2(project.filesDir) + sep)) throw new DevkitError(`"${name}" is not inside the files folder`);
|
|
416
|
+
return path;
|
|
417
|
+
}
|
|
418
|
+
function writeLocalFile(project, name, bytes) {
|
|
419
|
+
const path = localFilePath(project, name);
|
|
420
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
421
|
+
writeFileSync3(path, bytes);
|
|
422
|
+
return path;
|
|
423
|
+
}
|
|
279
424
|
export {
|
|
280
425
|
DevkitError,
|
|
426
|
+
collectPartials,
|
|
281
427
|
contentHash,
|
|
428
|
+
contentTypeFor,
|
|
282
429
|
defaultData,
|
|
283
430
|
defaultProjectConfig,
|
|
284
431
|
diagnose,
|
|
285
432
|
findProject,
|
|
433
|
+
listLocalFiles,
|
|
286
434
|
listTemplateSlugs,
|
|
287
435
|
loadProject,
|
|
436
|
+
localFilePath,
|
|
288
437
|
partialResolver,
|
|
289
438
|
previewDocument,
|
|
290
439
|
projectAround,
|
|
@@ -293,9 +442,12 @@ export {
|
|
|
293
442
|
recordSync,
|
|
294
443
|
renderContext,
|
|
295
444
|
renderLocal,
|
|
445
|
+
sha256File,
|
|
296
446
|
templateDir,
|
|
297
447
|
titleFromSlug,
|
|
298
448
|
versionPayload,
|
|
449
|
+
writeLocalFile,
|
|
450
|
+
writePartials,
|
|
299
451
|
writeProjectConfig,
|
|
300
452
|
writeState,
|
|
301
453
|
writeTemplate
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formfeed/devkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Formfeed template folders, project config and local rendering, shared by the CLI and @formfeed/testing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
],
|
|
31
31
|
"sideEffects": false,
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@formfeed/engine": "0.
|
|
34
|
-
"@formfeed/sdk": "0.
|
|
33
|
+
"@formfeed/engine": "0.2.0",
|
|
34
|
+
"@formfeed/sdk": "0.2.0"
|
|
35
35
|
},
|
|
36
36
|
"keywords": [
|
|
37
37
|
"formfeed",
|
package/types/index.d.cts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export { DevkitError } from './lib/errors.cjs';
|
|
2
2
|
export { defaultProjectConfig, findProject, loadProject, projectAround, writeProjectConfig } from './lib/project-config.cjs';
|
|
3
3
|
export type { Project, ProjectConfig } from './lib/project-config.cjs';
|
|
4
|
-
export { contentHash, defaultData, listTemplateSlugs, partialResolver, readState, readTemplate, recordSync, templateDir, titleFromSlug, versionPayload, writeState, writeTemplate, } from './lib/project.cjs';
|
|
4
|
+
export { contentHash, defaultData, listTemplateSlugs, partialResolver, readState, readTemplate, recordSync, templateDir, titleFromSlug, versionPayload, writePartials, writeState, writeTemplate, } from './lib/project.cjs';
|
|
5
5
|
export type { LocalTemplate, ProjectState, TemplateMeta, TemplateState } from './lib/project.cjs';
|
|
6
|
+
export { collectPartials } from './lib/partials.cjs';
|
|
7
|
+
export type { CollectedPartials } from './lib/partials.cjs';
|
|
6
8
|
export { diagnose, previewDocument, renderContext, renderLocal } from './lib/local-render.cjs';
|
|
9
|
+
export { contentTypeFor, listLocalFiles, localFilePath, sha256File, writeLocalFile } from './lib/files.cjs';
|
|
10
|
+
export type { LocalFile, LocalFileListing } from './lib/files.cjs';
|
|
7
11
|
export type { LocalRenderOptions } from './lib/local-render.cjs';
|
package/types/index.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export { DevkitError } from './lib/errors.js';
|
|
2
2
|
export { defaultProjectConfig, findProject, loadProject, projectAround, writeProjectConfig } from './lib/project-config.js';
|
|
3
3
|
export type { Project, ProjectConfig } from './lib/project-config.js';
|
|
4
|
-
export { contentHash, defaultData, listTemplateSlugs, partialResolver, readState, readTemplate, recordSync, templateDir, titleFromSlug, versionPayload, writeState, writeTemplate, } from './lib/project.js';
|
|
4
|
+
export { contentHash, defaultData, listTemplateSlugs, partialResolver, readState, readTemplate, recordSync, templateDir, titleFromSlug, versionPayload, writePartials, writeState, writeTemplate, } from './lib/project.js';
|
|
5
5
|
export type { LocalTemplate, ProjectState, TemplateMeta, TemplateState } from './lib/project.js';
|
|
6
|
+
export { collectPartials } from './lib/partials.js';
|
|
7
|
+
export type { CollectedPartials } from './lib/partials.js';
|
|
6
8
|
export { diagnose, previewDocument, renderContext, renderLocal } from './lib/local-render.js';
|
|
9
|
+
export { contentTypeFor, listLocalFiles, localFilePath, sha256File, writeLocalFile } from './lib/files.js';
|
|
10
|
+
export type { LocalFile, LocalFileListing } from './lib/files.js';
|
|
7
11
|
export type { LocalRenderOptions } from './lib/local-render.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Project } from './project-config.cjs';
|
|
2
|
+
/**
|
|
3
|
+
* The project's `files/` folder: the local copy of the workspace file library (spec 04 §2.3). A
|
|
4
|
+
* file's path below the folder is its library name, so `files/brand/logo.png` is what a template
|
|
5
|
+
* reaches with `asset('brand/logo.png')`, locally in `formfeed dev` and remotely after `files push`.
|
|
6
|
+
*/
|
|
7
|
+
export interface LocalFile {
|
|
8
|
+
/** The library name: the path below the files folder, with `/`. */
|
|
9
|
+
name: string;
|
|
10
|
+
path: string;
|
|
11
|
+
bytes: number;
|
|
12
|
+
sha256: string;
|
|
13
|
+
/** From the extension; null for a type the library does not take. */
|
|
14
|
+
contentType: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface LocalFileListing {
|
|
17
|
+
files: LocalFile[];
|
|
18
|
+
/** Paths the library would refuse: an unusable name, an unknown type, too large or empty. */
|
|
19
|
+
skipped: Array<{
|
|
20
|
+
path: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
/** The content type the library stores for a name, going by its extension. */
|
|
25
|
+
export declare function contentTypeFor(name: string): string | null;
|
|
26
|
+
export declare function sha256File(bytes: Uint8Array): string;
|
|
27
|
+
/** Every file below the files folder, with the ones the library would refuse listed apart. */
|
|
28
|
+
export declare function listLocalFiles(project: Project): LocalFileListing;
|
|
29
|
+
/** The path of a library name inside the files folder; refuses a name that would leave it. */
|
|
30
|
+
export declare function localFilePath(project: Project, name: string): string;
|
|
31
|
+
/** Writes a pulled file below the files folder, creating folders for names like `brand/logo.png`. */
|
|
32
|
+
export declare function writeLocalFile(project: Project, name: string, bytes: Uint8Array): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Project } from './project-config.js';
|
|
2
|
+
/**
|
|
3
|
+
* The project's `files/` folder: the local copy of the workspace file library (spec 04 §2.3). A
|
|
4
|
+
* file's path below the folder is its library name, so `files/brand/logo.png` is what a template
|
|
5
|
+
* reaches with `asset('brand/logo.png')`, locally in `formfeed dev` and remotely after `files push`.
|
|
6
|
+
*/
|
|
7
|
+
export interface LocalFile {
|
|
8
|
+
/** The library name: the path below the files folder, with `/`. */
|
|
9
|
+
name: string;
|
|
10
|
+
path: string;
|
|
11
|
+
bytes: number;
|
|
12
|
+
sha256: string;
|
|
13
|
+
/** From the extension; null for a type the library does not take. */
|
|
14
|
+
contentType: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface LocalFileListing {
|
|
17
|
+
files: LocalFile[];
|
|
18
|
+
/** Paths the library would refuse: an unusable name, an unknown type, too large or empty. */
|
|
19
|
+
skipped: Array<{
|
|
20
|
+
path: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
/** The content type the library stores for a name, going by its extension. */
|
|
25
|
+
export declare function contentTypeFor(name: string): string | null;
|
|
26
|
+
export declare function sha256File(bytes: Uint8Array): string;
|
|
27
|
+
/** Every file below the files folder, with the ones the library would refuse listed apart. */
|
|
28
|
+
export declare function listLocalFiles(project: Project): LocalFileListing;
|
|
29
|
+
/** The path of a library name inside the files folder; refuses a name that would leave it. */
|
|
30
|
+
export declare function localFilePath(project: Project, name: string): string;
|
|
31
|
+
/** Writes a pulled file below the files folder, creating folders for names like `brand/logo.png`. */
|
|
32
|
+
export declare function writeLocalFile(project: Project, name: string, bytes: Uint8Array): string;
|
|
@@ -3,11 +3,18 @@ import type { Project } from './project-config.cjs';
|
|
|
3
3
|
import { type LocalTemplate } from './project.cjs';
|
|
4
4
|
/** The same diagnostics the editor shows: analysis of the body against the sample data plus compile errors. */
|
|
5
5
|
export declare function diagnose(tpl: LocalTemplate, sampleData: unknown): Diagnostic[];
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* `assetBaseUrl` is where `asset()` and relative image paths resolve: the dev server's `/files`
|
|
8
|
+
* locally, the workspace library's CDN base when the document goes to the API. Without it the
|
|
9
|
+
* references stay relative, which keeps snapshots independent of any host.
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderContext(project: Project, tpl: LocalTemplate, assetBaseUrl?: string): RenderContext;
|
|
7
12
|
export interface LocalRenderOptions {
|
|
8
13
|
mode: 'preview' | 'print';
|
|
9
14
|
locale?: string;
|
|
10
15
|
vendor?: AssembleVendor;
|
|
16
|
+
/** Where `asset()` and relative image paths resolve (see `renderContext`). */
|
|
17
|
+
assetBaseUrl?: string;
|
|
11
18
|
}
|
|
12
19
|
/** Assembles the complete document with the shared engine; no browser involved. */
|
|
13
20
|
export declare function renderLocal(project: Project, tpl: LocalTemplate, data: unknown, options: LocalRenderOptions): Promise<RenderedDocument>;
|
|
@@ -3,11 +3,18 @@ import type { Project } from './project-config.js';
|
|
|
3
3
|
import { type LocalTemplate } from './project.js';
|
|
4
4
|
/** The same diagnostics the editor shows: analysis of the body against the sample data plus compile errors. */
|
|
5
5
|
export declare function diagnose(tpl: LocalTemplate, sampleData: unknown): Diagnostic[];
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* `assetBaseUrl` is where `asset()` and relative image paths resolve: the dev server's `/files`
|
|
8
|
+
* locally, the workspace library's CDN base when the document goes to the API. Without it the
|
|
9
|
+
* references stay relative, which keeps snapshots independent of any host.
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderContext(project: Project, tpl: LocalTemplate, assetBaseUrl?: string): RenderContext;
|
|
7
12
|
export interface LocalRenderOptions {
|
|
8
13
|
mode: 'preview' | 'print';
|
|
9
14
|
locale?: string;
|
|
10
15
|
vendor?: AssembleVendor;
|
|
16
|
+
/** Where `asset()` and relative image paths resolve (see `renderContext`). */
|
|
17
|
+
assetBaseUrl?: string;
|
|
11
18
|
}
|
|
12
19
|
/** Assembles the complete document with the shared engine; no browser involved. */
|
|
13
20
|
export declare function renderLocal(project: Project, tpl: LocalTemplate, data: unknown, options: LocalRenderOptions): Promise<RenderedDocument>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type EngineId } from '@formfeed/engine';
|
|
2
|
+
import type { Project } from './project-config.cjs';
|
|
3
|
+
/**
|
|
4
|
+
* The partials a template includes, read from the project's `partials/` folder. They travel with
|
|
5
|
+
* the version (`formfeed templates push`), so a published template resolves `{% include %}` in the
|
|
6
|
+
* API and the editor, not only in the CLI.
|
|
7
|
+
*
|
|
8
|
+
* Includes nest, so every partial is analysed in turn; a name that has no file is reported instead
|
|
9
|
+
* of failing, and `validate` and `push` show it.
|
|
10
|
+
*/
|
|
11
|
+
export interface CollectedPartials {
|
|
12
|
+
/** name → source, the shape the API stores. */
|
|
13
|
+
partials: Record<string, string>;
|
|
14
|
+
/** Names no file in the partials folder matches. */
|
|
15
|
+
missing: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function collectPartials(project: Project, engine: EngineId, sources: Array<string | undefined>): CollectedPartials;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type EngineId } from '@formfeed/engine';
|
|
2
|
+
import type { Project } from './project-config.js';
|
|
3
|
+
/**
|
|
4
|
+
* The partials a template includes, read from the project's `partials/` folder. They travel with
|
|
5
|
+
* the version (`formfeed templates push`), so a published template resolves `{% include %}` in the
|
|
6
|
+
* API and the editor, not only in the CLI.
|
|
7
|
+
*
|
|
8
|
+
* Includes nest, so every partial is analysed in turn; a name that has no file is reported instead
|
|
9
|
+
* of failing, and `validate` and `push` show it.
|
|
10
|
+
*/
|
|
11
|
+
export interface CollectedPartials {
|
|
12
|
+
/** name → source, the shape the API stores. */
|
|
13
|
+
partials: Record<string, string>;
|
|
14
|
+
/** Names no file in the partials folder matches. */
|
|
15
|
+
missing: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function collectPartials(project: Project, engine: EngineId, sources: Array<string | undefined>): CollectedPartials;
|
|
@@ -5,6 +5,8 @@ export interface ProjectConfig {
|
|
|
5
5
|
region?: 'eu' | 'us';
|
|
6
6
|
templatesDir: string;
|
|
7
7
|
partialsDir: string;
|
|
8
|
+
/** The local copy of the workspace file library (`formfeed files push|pull`). */
|
|
9
|
+
filesDir: string;
|
|
8
10
|
engine: EngineId;
|
|
9
11
|
ignore: string[];
|
|
10
12
|
}
|
|
@@ -15,6 +17,7 @@ export interface Project {
|
|
|
15
17
|
config: ProjectConfig;
|
|
16
18
|
templatesDir: string;
|
|
17
19
|
partialsDir: string;
|
|
20
|
+
filesDir: string;
|
|
18
21
|
}
|
|
19
22
|
export declare const defaultProjectConfig: ProjectConfig;
|
|
20
23
|
/** Walks up from `cwd` to the nearest `formfeed.json`. */
|
|
@@ -5,6 +5,8 @@ export interface ProjectConfig {
|
|
|
5
5
|
region?: 'eu' | 'us';
|
|
6
6
|
templatesDir: string;
|
|
7
7
|
partialsDir: string;
|
|
8
|
+
/** The local copy of the workspace file library (`formfeed files push|pull`). */
|
|
9
|
+
filesDir: string;
|
|
8
10
|
engine: EngineId;
|
|
9
11
|
ignore: string[];
|
|
10
12
|
}
|
|
@@ -15,6 +17,7 @@ export interface Project {
|
|
|
15
17
|
config: ProjectConfig;
|
|
16
18
|
templatesDir: string;
|
|
17
19
|
partialsDir: string;
|
|
20
|
+
filesDir: string;
|
|
18
21
|
}
|
|
19
22
|
export declare const defaultProjectConfig: ProjectConfig;
|
|
20
23
|
/** Walks up from `cwd` to the nearest `formfeed.json`. */
|
package/types/lib/project.d.cts
CHANGED
|
@@ -27,6 +27,10 @@ export interface LocalTemplate {
|
|
|
27
27
|
dataSets: Record<string, unknown>;
|
|
28
28
|
dataSchema: Record<string, unknown> | null;
|
|
29
29
|
i18n: Record<string, Record<string, string>> | null;
|
|
30
|
+
/** Partials the template includes, read from the project's folder; they travel with the version. */
|
|
31
|
+
partials: Record<string, string>;
|
|
32
|
+
/** Included names without a file; `validate` reports them and `push` refuses them. */
|
|
33
|
+
missingPartials: string[];
|
|
30
34
|
}
|
|
31
35
|
export interface TemplateState {
|
|
32
36
|
/** Checksum of the remote version the folder was pulled from or pushed as. */
|
|
@@ -50,9 +54,17 @@ export declare function defaultData(tpl: LocalTemplate, name?: string): {
|
|
|
50
54
|
};
|
|
51
55
|
export declare function writeTemplate(project: Project, slug: string, meta: TemplateMeta, version: Pick<TemplateVersion, 'html' | 'css' | 'head' | 'settings' | 'sample_data' | 'data_schema' | 'i18n'> & {
|
|
52
56
|
data_sets?: Record<string, unknown> | null;
|
|
57
|
+
partials?: Record<string, string> | null;
|
|
53
58
|
}): string;
|
|
59
|
+
/**
|
|
60
|
+
* Writes the partials of a pulled version into the project's folder (`name` or `name.html`, the
|
|
61
|
+
* two spellings `partialResolver` reads). Returns the files whose content changed, so `pull` can
|
|
62
|
+
* say that a partial shared with another template was replaced.
|
|
63
|
+
*/
|
|
64
|
+
export declare function writePartials(project: Project, partials: Record<string, string> | null): string[];
|
|
54
65
|
/** Files of a push request: what the API's TemplateVersionCreate takes. */
|
|
55
66
|
export declare function versionPayload(tpl: LocalTemplate): {
|
|
67
|
+
partials?: Record<string, string> | undefined;
|
|
56
68
|
html: string;
|
|
57
69
|
css: string;
|
|
58
70
|
head: string;
|
package/types/lib/project.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export interface LocalTemplate {
|
|
|
27
27
|
dataSets: Record<string, unknown>;
|
|
28
28
|
dataSchema: Record<string, unknown> | null;
|
|
29
29
|
i18n: Record<string, Record<string, string>> | null;
|
|
30
|
+
/** Partials the template includes, read from the project's folder; they travel with the version. */
|
|
31
|
+
partials: Record<string, string>;
|
|
32
|
+
/** Included names without a file; `validate` reports them and `push` refuses them. */
|
|
33
|
+
missingPartials: string[];
|
|
30
34
|
}
|
|
31
35
|
export interface TemplateState {
|
|
32
36
|
/** Checksum of the remote version the folder was pulled from or pushed as. */
|
|
@@ -50,9 +54,17 @@ export declare function defaultData(tpl: LocalTemplate, name?: string): {
|
|
|
50
54
|
};
|
|
51
55
|
export declare function writeTemplate(project: Project, slug: string, meta: TemplateMeta, version: Pick<TemplateVersion, 'html' | 'css' | 'head' | 'settings' | 'sample_data' | 'data_schema' | 'i18n'> & {
|
|
52
56
|
data_sets?: Record<string, unknown> | null;
|
|
57
|
+
partials?: Record<string, string> | null;
|
|
53
58
|
}): string;
|
|
59
|
+
/**
|
|
60
|
+
* Writes the partials of a pulled version into the project's folder (`name` or `name.html`, the
|
|
61
|
+
* two spellings `partialResolver` reads). Returns the files whose content changed, so `pull` can
|
|
62
|
+
* say that a partial shared with another template was replaced.
|
|
63
|
+
*/
|
|
64
|
+
export declare function writePartials(project: Project, partials: Record<string, string> | null): string[];
|
|
54
65
|
/** Files of a push request: what the API's TemplateVersionCreate takes. */
|
|
55
66
|
export declare function versionPayload(tpl: LocalTemplate): {
|
|
67
|
+
partials?: Record<string, string> | undefined;
|
|
56
68
|
html: string;
|
|
57
69
|
css: string;
|
|
58
70
|
head: string;
|