@formfeed/devkit 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import type { EngineId, TemplateKind, TemplateSettings } from '@formfeed/engine';
1
+ import { type EngineId, type OfficeKind, type TemplateKind, type TemplateSettings } from '@formfeed/engine';
2
2
  import type { TemplateVersion } from '@formfeed/sdk';
3
3
  import type { Project } from './project-config.js';
4
4
  /**
@@ -9,15 +9,29 @@ import type { Project } from './project-config.js';
9
9
  */
10
10
  export interface TemplateMeta {
11
11
  name: string;
12
- kind: TemplateKind;
12
+ /** An HTML kind, or `docx`/`pptx` for Word and PowerPoint templates. */
13
+ kind: TemplateKind | OfficeKind;
13
14
  engine: EngineId;
14
15
  description?: string | null;
15
16
  tags?: string[];
16
17
  }
18
+ /**
19
+ * The document of a Word or PowerPoint template (spec 22 §7): `template.docx` or `template.pptx`
20
+ * instead of `template.html`. Such a folder has no markup, style, head, header, footer or partials.
21
+ */
22
+ export interface LocalOfficeFile {
23
+ format: OfficeKind;
24
+ path: string;
25
+ bytes: Uint8Array;
26
+ sha256: string;
27
+ }
17
28
  export interface LocalTemplate {
18
29
  slug: string;
19
30
  dir: string;
20
31
  meta: TemplateMeta;
32
+ /** The Word or PowerPoint document; `null` for HTML templates. */
33
+ file: LocalOfficeFile | null;
34
+ /** Empty for Word and PowerPoint templates. */
21
35
  html: string;
22
36
  css: string;
23
37
  head: string;
@@ -27,6 +41,15 @@ export interface LocalTemplate {
27
41
  dataSets: Record<string, unknown>;
28
42
  dataSchema: Record<string, unknown> | null;
29
43
  i18n: Record<string, Record<string, string>> | null;
44
+ /** Partials the template includes, read from the project's folder; they travel with the version. */
45
+ partials: Record<string, string>;
46
+ /** Included names without a file; `validate` reports them and `push` refuses them. */
47
+ missingPartials: string[];
48
+ /**
49
+ * Included names that are shared partials of the organisation (recorded in the state by
50
+ * `formfeed partials pull|push`). They resolve live on the server, so they stay out of `partials`.
51
+ */
52
+ sharedPartials: string[];
30
53
  }
31
54
  export interface TemplateState {
32
55
  /** Checksum of the remote version the folder was pulled from or pushed as. */
@@ -35,11 +58,26 @@ export interface TemplateState {
35
58
  status: string;
36
59
  /** Hash of the local files at that moment; a different hash means local edits. */
37
60
  contentHash: string;
61
+ /** Word and PowerPoint templates: the document's SHA-256, so `push` uploads it only when it changed. */
62
+ fileSha256?: string;
63
+ syncedAt: string;
64
+ }
65
+ /** A shared partial of the organisation as last pulled or pushed (spec 18 §8). */
66
+ export interface SharedPartialState {
67
+ /** The remote version; `partials push` sends it as `base_version`. */
68
+ version: number;
69
+ engine: EngineId;
70
+ /** Hash of the local file at that moment; a different hash means local edits. */
71
+ contentHash: string;
38
72
  syncedAt: string;
39
73
  }
40
74
  export interface ProjectState {
41
75
  templates: Record<string, TemplateState>;
76
+ /** Shared partials by name; they live in `partialsDir` as `<name>.html`. */
77
+ sharedPartials?: Record<string, SharedPartialState>;
42
78
  }
79
+ /** The file a template folder is built around, by kind. */
80
+ export declare const templateFileName: (kind: TemplateKind | string) => string;
43
81
  export declare function listTemplateSlugs(project: Project): string[];
44
82
  export declare function templateDir(project: Project, slug: string): string;
45
83
  export declare function readTemplate(project: Project, slug: string): LocalTemplate;
@@ -48,19 +86,43 @@ export declare function defaultData(tpl: LocalTemplate, name?: string): {
48
86
  name: string;
49
87
  data: unknown;
50
88
  };
89
+ /**
90
+ * Writes a pulled version into the template folder. A Word or PowerPoint template needs its
91
+ * document as `file`; the folder then holds `template.<kind>` and none of the HTML files, so a
92
+ * template whose kind changed leaves nothing behind that would be read as its source.
93
+ */
51
94
  export declare function writeTemplate(project: Project, slug: string, meta: TemplateMeta, version: Pick<TemplateVersion, 'html' | 'css' | 'head' | 'settings' | 'sample_data' | 'data_schema' | 'i18n'> & {
52
95
  data_sets?: Record<string, unknown> | null;
53
- }): string;
96
+ partials?: Record<string, string> | null;
97
+ }, file?: Uint8Array): string;
98
+ /**
99
+ * Writes the partials of a pulled version into the project's folder (`name` or `name.html`, the
100
+ * two spellings `partialResolver` reads). Returns the files whose content changed, so `pull` can
101
+ * say that a partial shared with another template was replaced.
102
+ */
103
+ export declare function writePartials(project: Project, partials: Record<string, string> | null): string[];
104
+ /**
105
+ * Fields of a push request besides the document: what the API's version create takes for a Word or
106
+ * PowerPoint template. The document itself travels as the multipart `file`.
107
+ */
108
+ export declare function officeVersionPayload(tpl: LocalTemplate): {
109
+ settings: Record<string, unknown>;
110
+ sample_data: Record<string, unknown>;
111
+ data_sets: Record<string, unknown>;
112
+ data_schema: Record<string, unknown> | null;
113
+ i18n: Record<string, Record<string, string>> | null;
114
+ };
54
115
  /** Files of a push request: what the API's TemplateVersionCreate takes. */
55
116
  export declare function versionPayload(tpl: LocalTemplate): {
56
- html: string;
57
- css: string;
58
- head: string;
117
+ partials?: Record<string, string> | undefined;
59
118
  settings: Record<string, unknown>;
60
119
  sample_data: Record<string, unknown>;
61
120
  data_sets: Record<string, unknown>;
62
121
  data_schema: Record<string, unknown> | null;
63
122
  i18n: Record<string, Record<string, string>> | null;
123
+ html: string;
124
+ css: string;
125
+ head: string;
64
126
  };
65
127
  /** Stable hash of the local files; compared with the state to detect local edits. */
66
128
  export declare function contentHash(tpl: LocalTemplate): string;
@@ -68,5 +130,19 @@ export declare function readState(project: Project): ProjectState;
68
130
  export declare function writeState(project: Project, state: ProjectState): void;
69
131
  export declare function recordSync(project: Project, slug: string, version: Pick<TemplateVersion, 'checksum' | 'number' | 'status'>, tpl: LocalTemplate): void;
70
132
  export declare function titleFromSlug(slug: string): string;
71
- /** Resolves `{% include %}` / partials from the project's partials folder (`name` or `name.html`). */
133
+ /**
134
+ * Resolves `{% include %}` / partials from the project's partials folder (`name` or `name.html`).
135
+ * A name that matches a shared partial of the state only after normalisation (`Letterhead.j2` for
136
+ * `letterhead`) falls back to that partial's file, the way the server matches shared partials.
137
+ */
72
138
  export declare function partialResolver(project: Project): (name: string) => string | undefined;
139
+ /** Where a shared partial lives locally: `<partialsDir>/<name>.html`. */
140
+ export declare function sharedPartialPath(project: Project, name: string): string;
141
+ /** Normalised names of the shared partials recorded for one engine; other engines' partials never resolve. */
142
+ export declare function sharedPartialNames(project: Project, engine: EngineId): Set<string>;
143
+ export declare const partialContentHash: (source: string) => string;
144
+ /** Records a pulled or pushed shared partial with the remote version and the hash of `source`. */
145
+ export declare function recordSharedPartial(project: Project, name: string, remote: {
146
+ version: number;
147
+ engine: EngineId;
148
+ }, source: string): void;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shape-keeping redaction of render data (spec 20 §3.3), for `formfeed renders pull --redact`.
3
+ *
4
+ * Strings become placeholders of the same length that keep the character classes: upper-case
5
+ * letters become `A`, other letters `a`, digits `0`, and everything else (spaces, `@`, `.`, `-`)
6
+ * stays, so `jane@mail.test` becomes `aaaa@aaaa.aaaa` and `DE00 0000` becomes `AA00 0000`. Numbers,
7
+ * booleans and null are kept: they are usually what made the render go wrong, and they rarely
8
+ * identify a person. Arrays and objects keep their keys and lengths.
9
+ *
10
+ * Paths select what is redacted, from the root of the data: dotted segments (`customer.email`),
11
+ * `*` for any one key or array item (`customer.*`), and `[]` for every item of an array
12
+ * (`items[].name`, the same as `items.*.name`). A path that ends on an object or array redacts
13
+ * everything below it. Without paths (or with an empty list) the whole data set is redacted.
14
+ */
15
+ export declare function redactData<T>(value: T, paths?: readonly string[]): T;
16
+ /** A placeholder of the same length with the character classes of `text` kept. */
17
+ export declare function redactString(text: string): string;
18
+ /** `items[].name` → `['items', '*', 'name']`; `lines[0].qty` → `['lines', '0', 'qty']`. */
19
+ export declare function parseRedactPath(path: string): string[];
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shape-keeping redaction of render data (spec 20 §3.3), for `formfeed renders pull --redact`.
3
+ *
4
+ * Strings become placeholders of the same length that keep the character classes: upper-case
5
+ * letters become `A`, other letters `a`, digits `0`, and everything else (spaces, `@`, `.`, `-`)
6
+ * stays, so `jane@mail.test` becomes `aaaa@aaaa.aaaa` and `DE00 0000` becomes `AA00 0000`. Numbers,
7
+ * booleans and null are kept: they are usually what made the render go wrong, and they rarely
8
+ * identify a person. Arrays and objects keep their keys and lengths.
9
+ *
10
+ * Paths select what is redacted, from the root of the data: dotted segments (`customer.email`),
11
+ * `*` for any one key or array item (`customer.*`), and `[]` for every item of an array
12
+ * (`items[].name`, the same as `items.*.name`). A path that ends on an object or array redacts
13
+ * everything below it. Without paths (or with an empty list) the whole data set is redacted.
14
+ */
15
+ export declare function redactData<T>(value: T, paths?: readonly string[]): T;
16
+ /** A placeholder of the same length with the character classes of `text` kept. */
17
+ export declare function redactString(text: string): string;
18
+ /** `items[].name` → `['items', '*', 'name']`; `lines[0].qty` → `['lines', '0', 'qty']`. */
19
+ export declare function parseRedactPath(path: string): string[];
@@ -0,0 +1,6 @@
1
+ import { type TemplateTypes } from './model.cjs';
2
+ /**
3
+ * `formfeed_templates.py`: a TypedDict per template and `typed(client)`, whose `renders.create`
4
+ * overloads check `data` for the known slugs and fall back to the plain signature otherwise.
5
+ */
6
+ export declare function emitPython(templates: TemplateTypes[]): string;
@@ -0,0 +1,6 @@
1
+ import { type TemplateTypes } from './model.js';
2
+ /**
3
+ * `formfeed_templates.py`: a TypedDict per template and `typed(client)`, whose `renders.create`
4
+ * overloads check `data` for the known slugs and fall back to the plain signature otherwise.
5
+ */
6
+ export declare function emitPython(templates: TemplateTypes[]): string;
@@ -0,0 +1,10 @@
1
+ import { type TemplateTypes } from './model.cjs';
2
+ export interface TypeScriptOptions {
3
+ /** The module the generated file augments; `@formfeed/sdk` for customers. */
4
+ sdkModule?: string;
5
+ }
6
+ /**
7
+ * `formfeed.d.ts`: one data type per template plus the augmentation of `FormfeedTemplates`, which
8
+ * makes `renders.create({ template: 'invoice', data })` check `data` against `InvoiceData`.
9
+ */
10
+ export declare function emitTypeScript(templates: TemplateTypes[], options?: TypeScriptOptions): string;
@@ -0,0 +1,10 @@
1
+ import { type TemplateTypes } from './model.js';
2
+ export interface TypeScriptOptions {
3
+ /** The module the generated file augments; `@formfeed/sdk` for customers. */
4
+ sdkModule?: string;
5
+ }
6
+ /**
7
+ * `formfeed.d.ts`: one data type per template plus the augmentation of `FormfeedTemplates`, which
8
+ * makes `renders.create({ template: 'invoice', data })` check `data` against `InvoiceData`.
9
+ */
10
+ export declare function emitTypeScript(templates: TemplateTypes[], options?: TypeScriptOptions): string;
@@ -0,0 +1,15 @@
1
+ import type { LocalTemplate } from '../project.cjs';
2
+ import { type TypeScriptOptions } from './emit-ts.cjs';
3
+ import { type TypeSource } from './model.cjs';
4
+ export type TypesLanguage = 'ts' | 'python';
5
+ export declare const defaultTypesFile: Record<TypesLanguage, string>;
6
+ /**
7
+ * The schema of a template folder: `schema.json` when it has one, else inferred over all its data
8
+ * sets (a field only some sets carry is optional), marked as inferred so every field is optional.
9
+ */
10
+ export declare function localTypeSource(tpl: LocalTemplate): TypeSource;
11
+ /** The generated file for `sources` in `language`; the same input always gives the same bytes. */
12
+ export declare function generateTypes(sources: TypeSource[], language: TypesLanguage, options?: TypeScriptOptions): string;
13
+ export { buildTypes, pascalCase, relaxRequired, singular } from './model.cjs';
14
+ export type { SchemaOrigin, TemplateTypes, TypeNode, TypeSource } from './model.cjs';
15
+ export type { TypeScriptOptions } from './emit-ts.cjs';
@@ -0,0 +1,15 @@
1
+ import type { LocalTemplate } from '../project.js';
2
+ import { type TypeScriptOptions } from './emit-ts.js';
3
+ import { type TypeSource } from './model.js';
4
+ export type TypesLanguage = 'ts' | 'python';
5
+ export declare const defaultTypesFile: Record<TypesLanguage, string>;
6
+ /**
7
+ * The schema of a template folder: `schema.json` when it has one, else inferred over all its data
8
+ * sets (a field only some sets carry is optional), marked as inferred so every field is optional.
9
+ */
10
+ export declare function localTypeSource(tpl: LocalTemplate): TypeSource;
11
+ /** The generated file for `sources` in `language`; the same input always gives the same bytes. */
12
+ export declare function generateTypes(sources: TypeSource[], language: TypesLanguage, options?: TypeScriptOptions): string;
13
+ export { buildTypes, pascalCase, relaxRequired, singular } from './model.js';
14
+ export type { SchemaOrigin, TemplateTypes, TypeNode, TypeSource } from './model.js';
15
+ export type { TypeScriptOptions } from './emit-ts.js';
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The language-neutral model `formfeed types` emits from (spec 19 §2.4): each template's data schema
3
+ * becomes a tree of type nodes plus the named objects it refers to, so the TypeScript and Python
4
+ * emitters share one reading of JSON Schema and differ only in spelling.
5
+ */
6
+ export type TypeNode = {
7
+ kind: 'unknown';
8
+ note?: string;
9
+ } | {
10
+ kind: 'never';
11
+ } | {
12
+ kind: 'primitive';
13
+ name: 'string' | 'number' | 'integer' | 'boolean' | 'null';
14
+ } | {
15
+ kind: 'literal';
16
+ value: string | number | boolean | null;
17
+ } | {
18
+ kind: 'array';
19
+ items: TypeNode;
20
+ } | {
21
+ kind: 'record';
22
+ values: TypeNode;
23
+ } | {
24
+ kind: 'ref';
25
+ name: string;
26
+ } | {
27
+ kind: 'union';
28
+ members: TypeNode[];
29
+ } | {
30
+ kind: 'intersection';
31
+ members: TypeNode[];
32
+ };
33
+ export interface Field {
34
+ key: string;
35
+ type: TypeNode;
36
+ required: boolean;
37
+ doc: string[];
38
+ }
39
+ export interface NamedObject {
40
+ name: string;
41
+ doc: string[];
42
+ fields: Field[];
43
+ /** Type of properties the schema does not list, when `additionalProperties` is a schema. */
44
+ extra: TypeNode | null;
45
+ }
46
+ /** Where a template's schema came from; the header of the generated file says it per template. */
47
+ export type SchemaOrigin = 'local' | 'stored' | 'inferred';
48
+ export interface TypeSource {
49
+ slug: string;
50
+ schema: Record<string, unknown>;
51
+ origin: SchemaOrigin;
52
+ /** The version the schema belongs to, for the header. */
53
+ version?: number | null;
54
+ channel?: string | null;
55
+ }
56
+ export interface TemplateTypes {
57
+ slug: string;
58
+ origin: SchemaOrigin;
59
+ version: number | null;
60
+ channel: string | null;
61
+ /** The name the data type is exported under (`InvoiceData`). */
62
+ rootName: string;
63
+ /** The data type; a `ref` to `rootName` when the schema is an object. */
64
+ root: TypeNode;
65
+ /** Named objects in the order they were first met, the root first. */
66
+ objects: NamedObject[];
67
+ }
68
+ export declare function pascalCase(text: string): string;
69
+ /** `items` → `Item`, `addresses` → `Address`, `entries` → `Entry`; null when the word is not a plural. */
70
+ export declare function singular(word: string): string | null;
71
+ /** Names already handed out in one generated file; a clash gets a number. */
72
+ export declare class NameRegistry {
73
+ private readonly used;
74
+ constructor(reserved?: Iterable<string>);
75
+ claim(wanted: string): string;
76
+ }
77
+ /** An inferred schema knows which fields the sample had, not which the template needs: all optional. */
78
+ export declare function relaxRequired(schema: unknown): unknown;
79
+ /** Builds the model for a set of templates; names are unique across the whole file. */
80
+ export declare function buildTypes(sources: TypeSource[], reserved?: Iterable<string>): TemplateTypes[];
81
+ export declare function originNote(t: Pick<TemplateTypes, 'origin' | 'version' | 'channel'>): string;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The language-neutral model `formfeed types` emits from (spec 19 §2.4): each template's data schema
3
+ * becomes a tree of type nodes plus the named objects it refers to, so the TypeScript and Python
4
+ * emitters share one reading of JSON Schema and differ only in spelling.
5
+ */
6
+ export type TypeNode = {
7
+ kind: 'unknown';
8
+ note?: string;
9
+ } | {
10
+ kind: 'never';
11
+ } | {
12
+ kind: 'primitive';
13
+ name: 'string' | 'number' | 'integer' | 'boolean' | 'null';
14
+ } | {
15
+ kind: 'literal';
16
+ value: string | number | boolean | null;
17
+ } | {
18
+ kind: 'array';
19
+ items: TypeNode;
20
+ } | {
21
+ kind: 'record';
22
+ values: TypeNode;
23
+ } | {
24
+ kind: 'ref';
25
+ name: string;
26
+ } | {
27
+ kind: 'union';
28
+ members: TypeNode[];
29
+ } | {
30
+ kind: 'intersection';
31
+ members: TypeNode[];
32
+ };
33
+ export interface Field {
34
+ key: string;
35
+ type: TypeNode;
36
+ required: boolean;
37
+ doc: string[];
38
+ }
39
+ export interface NamedObject {
40
+ name: string;
41
+ doc: string[];
42
+ fields: Field[];
43
+ /** Type of properties the schema does not list, when `additionalProperties` is a schema. */
44
+ extra: TypeNode | null;
45
+ }
46
+ /** Where a template's schema came from; the header of the generated file says it per template. */
47
+ export type SchemaOrigin = 'local' | 'stored' | 'inferred';
48
+ export interface TypeSource {
49
+ slug: string;
50
+ schema: Record<string, unknown>;
51
+ origin: SchemaOrigin;
52
+ /** The version the schema belongs to, for the header. */
53
+ version?: number | null;
54
+ channel?: string | null;
55
+ }
56
+ export interface TemplateTypes {
57
+ slug: string;
58
+ origin: SchemaOrigin;
59
+ version: number | null;
60
+ channel: string | null;
61
+ /** The name the data type is exported under (`InvoiceData`). */
62
+ rootName: string;
63
+ /** The data type; a `ref` to `rootName` when the schema is an object. */
64
+ root: TypeNode;
65
+ /** Named objects in the order they were first met, the root first. */
66
+ objects: NamedObject[];
67
+ }
68
+ export declare function pascalCase(text: string): string;
69
+ /** `items` → `Item`, `addresses` → `Address`, `entries` → `Entry`; null when the word is not a plural. */
70
+ export declare function singular(word: string): string | null;
71
+ /** Names already handed out in one generated file; a clash gets a number. */
72
+ export declare class NameRegistry {
73
+ private readonly used;
74
+ constructor(reserved?: Iterable<string>);
75
+ claim(wanted: string): string;
76
+ }
77
+ /** An inferred schema knows which fields the sample had, not which the template needs: all optional. */
78
+ export declare function relaxRequired(schema: unknown): unknown;
79
+ /** Builds the model for a set of templates; names are unique across the whole file. */
80
+ export declare function buildTypes(sources: TypeSource[], reserved?: Iterable<string>): TemplateTypes[];
81
+ export declare function originNote(t: Pick<TemplateTypes, 'origin' | 'version' | 'channel'>): string;