@sanoma-ai/workflows 0.4.1 → 0.5.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/README.md +19 -10
- package/dist/cedar.d.ts +23 -6
- package/dist/cedar.js +151 -44
- package/dist/cedar.js.map +1 -1
- package/dist/client.d.ts +3 -2
- package/dist/client.js +15 -6
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +6 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/describe.d.ts +2 -0
- package/dist/describe.js +39 -4
- package/dist/describe.js.map +1 -1
- package/dist/host.d.ts +2 -0
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/op.d.ts +57 -3
- package/dist/op.js +10 -2
- package/dist/op.js.map +1 -1
- package/dist/policy.d.ts +9 -1
- package/dist/policy.js +18 -7
- package/dist/policy.js.map +1 -1
- package/dist/resources.d.ts +4 -2
- package/dist/resources.js +23 -11
- package/dist/resources.js.map +1 -1
- package/dist/scenario.d.ts +1 -1
- package/dist/scenario.js +4 -2
- package/dist/scenario.js.map +1 -1
- package/dist/schema.js +1 -1
- package/dist/shared.d.ts +27 -2
- package/dist/shared.js +52 -3
- package/dist/shared.js.map +1 -1
- package/dist/sites.d.ts +35 -6
- package/dist/sites.js +129 -25
- package/dist/sites.js.map +1 -1
- package/dist/vendor-info.d.ts +19 -0
- package/dist/vendor-info.js +49 -0
- package/dist/vendor-info.js.map +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/src/cedar.ts +152 -41
- package/src/client.ts +17 -4
- package/src/config.ts +8 -2
- package/src/describe.ts +48 -4
- package/src/host.ts +2 -0
- package/src/index.ts +11 -0
- package/src/op.ts +61 -5
- package/src/policy.ts +29 -10
- package/src/resources.ts +26 -10
- package/src/scenario.ts +5 -2
- package/src/schema.ts +1 -1
- package/src/shared.ts +61 -3
- package/src/sites.ts +162 -30
- package/src/vendor-info.ts +53 -0
- package/src/version.ts +4 -2
package/src/sites.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join, relative, sep } from "node:path";
|
|
3
3
|
import { childrenOf, type LintProblem, type Node, readSource, type SourceFile } from "./ast.ts";
|
|
4
|
+
import { type CedarPolicySource, visitorPermits } from "./cedar.ts";
|
|
4
5
|
import { keyName } from "./datafile.ts";
|
|
5
6
|
import type { ConfigDescription } from "./describe.ts";
|
|
6
7
|
import { stringValue, unwrap } from "./outline.ts";
|
|
7
8
|
import { filesUnder, isDirectory } from "./paths.ts";
|
|
8
|
-
import {
|
|
9
|
+
import { START } from "./schema.ts";
|
|
10
|
+
import {
|
|
11
|
+
ANONYMOUS,
|
|
12
|
+
apiNameOf,
|
|
13
|
+
BUILT_IN_WORKFLOWS,
|
|
14
|
+
DATA_VENDOR,
|
|
15
|
+
SITE_NAME,
|
|
16
|
+
VISITORS,
|
|
17
|
+
WORKFLOW_ACTION,
|
|
18
|
+
} from "./shared.ts";
|
|
9
19
|
|
|
10
20
|
// What a company's sites call, read from their source with oxc and never run: the reverse index
|
|
11
21
|
// a workflow's page shows as Used by, and the names `sanoma check` holds to the config.
|
|
@@ -19,7 +29,13 @@ export interface SiteCall {
|
|
|
19
29
|
* `api` gives; that name itself when none is), the collection's name, or the operation's id.
|
|
20
30
|
*/
|
|
21
31
|
target: string;
|
|
22
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* What it calls of the workflow (`start`, or a read of its runs: `result`, `latest`, …) or the
|
|
34
|
+
* collection (`list`, `save`, …), as written; absent for a workflow called as itself or by
|
|
35
|
+
* `RunForm`, which starts it, and for an operation.
|
|
36
|
+
*/
|
|
37
|
+
method?: string;
|
|
38
|
+
/** `<RunForm workflow="…" />` or a call on `api`, from `sanoma/site`. */
|
|
23
39
|
via: "RunForm" | "api";
|
|
24
40
|
/** The file, relative to the config's root, with `/`: `sites/sales/routes/index.tsx`. */
|
|
25
41
|
file: string;
|
|
@@ -41,17 +57,26 @@ export interface SiteEntry {
|
|
|
41
57
|
name: string;
|
|
42
58
|
/** `defineSite({ title })` in its `site.ts`, when that is a string literal. */
|
|
43
59
|
title?: string;
|
|
60
|
+
/** `defineSite({ public: true })`: served to visitors, not people. */
|
|
61
|
+
public?: true;
|
|
62
|
+
/** `defineSite({ login })`, the connector whose login half signs visitors in, as written: one that names none is among `problems`. */
|
|
63
|
+
login?: string;
|
|
64
|
+
/** The host a public site answers on: set by `startApp` from its `domains`, never by `readSites`. */
|
|
65
|
+
domain?: string;
|
|
44
66
|
/** Its calls, by file and then in the file's order. */
|
|
45
67
|
calls: SiteCall[];
|
|
46
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Each call to a workflow, a collection or an operation the config does not have, or to an
|
|
70
|
+
* operation a page may not call directly, or, on a public site, that no policy lets a visitor
|
|
71
|
+
* make, at its file; then what is wrong in its `site.ts`.
|
|
72
|
+
*/
|
|
47
73
|
problems: (LintProblem & { file: string })[];
|
|
48
74
|
}
|
|
49
75
|
|
|
50
76
|
/** Where a site's code is: its pages and its components. */
|
|
51
77
|
const SITE_DIRS = ["routes", "components"];
|
|
52
|
-
/** Where `api` and `defineSite` come from: `sanoma/site`, which re-exports the app's. */
|
|
78
|
+
/** Where `api`, `RunForm` and `defineSite` come from: `sanoma/site`, which re-exports the app's. */
|
|
53
79
|
const SITE_SOURCES: ReadonlySet<string> = new Set(["sanoma/site", "@sanoma-ai/app/site"]);
|
|
54
|
-
const UI_SOURCES: ReadonlySet<string> = new Set(["@sanoma-ai/app/ui"]);
|
|
55
80
|
const KIND_NAMES = { workflow: "workflow", collection: "collection", op: "operation" } as const;
|
|
56
81
|
/** What makes a route from its id, whose first argument is the id. */
|
|
57
82
|
const ROUTE_MAKERS: ReadonlySet<string> = new Set(["createFileRoute", "createLazyFileRoute"]);
|
|
@@ -60,17 +85,25 @@ const ROUTE_MAKERS: ReadonlySet<string> = new Set(["createFileRoute", "createLaz
|
|
|
60
85
|
* The sites under `root`: each folder of `sites/` with a `routes/` whose name is a URL segment,
|
|
61
86
|
* by name (as `@sanoma-ai/app/build`'s `sitesOf` mounts them). Reads `routes/**` and
|
|
62
87
|
* `components/**` (`.ts` and `.tsx`, less tests and declarations) of each with oxc, never running
|
|
63
|
-
* them: each `<RunForm workflow="…" />`
|
|
64
|
-
*
|
|
88
|
+
* them: each `<RunForm workflow="…" />` and each use of `api`, both imported from `sanoma/site`,
|
|
89
|
+
* called or passed on (`queryFn: api.data.leads.list`):
|
|
65
90
|
* `api.<name>` and `api.<name>.<method>` are a workflow, `api.data.<collection>.<op>` a collection,
|
|
66
91
|
* and `api.<vendor>.<resource>.<op>` an operation. A name that is not written out (a variable, a
|
|
67
92
|
* template with holes, a computed member) is skipped, as is a use through a namespace import or a
|
|
68
93
|
* destructured `api`. A call to what `description` does not have, or to an operation a page may
|
|
69
|
-
* not call directly, is a problem.
|
|
94
|
+
* not call directly, is a problem. So is a `login` in `site.ts` that does not name one of
|
|
95
|
+
* `connectors.logins` (the vendors whose connector has a login half) on a public site. Given the
|
|
96
|
+
* company's `policies` (undefined while they have problems, which refuse every call), so is a
|
|
97
|
+
* public site's start or direct call that no company `permit` could let a visitor make
|
|
98
|
+
* (`visitorPermits`): one for `anonymous`, or on a site with a login, one for `anonymous` or the
|
|
99
|
+
* `visitors`, whatever its conditions. Whatever the policies, so is a public site's call of a
|
|
100
|
+
* built-in workflow, and its read of a workflow's runs but `.result`, which `callApi` refuses a visitor.
|
|
70
101
|
*/
|
|
71
102
|
export function readSites(
|
|
72
103
|
root: string | undefined,
|
|
73
104
|
description: Pick<ConfigDescription, "workflows" | "collections" | "ops">,
|
|
105
|
+
connectors: SiteConnectors,
|
|
106
|
+
policies?: readonly CedarPolicySource[],
|
|
74
107
|
): SiteEntry[] {
|
|
75
108
|
if (root === undefined) return [];
|
|
76
109
|
const dir = join(root, "sites");
|
|
@@ -80,6 +113,40 @@ export function readSites(
|
|
|
80
113
|
const ops = new Map(description.ops.map((op) => [op.id, op]));
|
|
81
114
|
// A workflow's api name to its name: two of one api name are a problem on the workflows.
|
|
82
115
|
const byApiName = new Map(description.workflows.map((wf) => [apiNameOf(wf.name), wf.name]));
|
|
116
|
+
const permits = policies && visitorPermits(policies);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Why a visitor's `call`, which exists, is certainly refused on a public site that has `login`:
|
|
120
|
+
* as `callApi` refuses it whatever the policy says, or as no permit could allow it.
|
|
121
|
+
*/
|
|
122
|
+
const refused = (call: SiteCall, login: string | undefined): string | undefined => {
|
|
123
|
+
const why = `a visitor's call to ${KIND_NAMES[call.kind]} "${call.target}" is refused`;
|
|
124
|
+
let action: string;
|
|
125
|
+
let group: string;
|
|
126
|
+
if (call.kind === "workflow") {
|
|
127
|
+
if (BUILT_IN_WORKFLOWS.has(call.target)) return `${why}: a visitor may not start the runtime's own workflows`;
|
|
128
|
+
if (call.method === "result") return undefined;
|
|
129
|
+
if (call.method !== undefined && call.method !== "start") {
|
|
130
|
+
return `${why}: a visitor reads only the result of a run it started`;
|
|
131
|
+
}
|
|
132
|
+
[action, group] = [`${WORKFLOW_ACTION}${call.target}`, START];
|
|
133
|
+
} else {
|
|
134
|
+
action = call.kind === "op" ? call.target : `${DATA_VENDOR}.${call.target}.${call.method}`;
|
|
135
|
+
const op = ops.get(action);
|
|
136
|
+
if (!op) return undefined;
|
|
137
|
+
group = `effect:${op.effect}`;
|
|
138
|
+
}
|
|
139
|
+
if (!permits) return undefined;
|
|
140
|
+
const [scopes, principal] =
|
|
141
|
+
login === undefined
|
|
142
|
+
? [[ANONYMOUS], `== Sanoma::Principal::"${ANONYMOUS}"`]
|
|
143
|
+
: [[ANONYMOUS, VISITORS], `in Sanoma::Group::"${VISITORS}"`];
|
|
144
|
+
if (permits(new Set(scopes), action, group)) return undefined;
|
|
145
|
+
return (
|
|
146
|
+
`${why}: no permit in policies/ names ${scopes.join(" or ")} for ${action}. ` +
|
|
147
|
+
`Add: permit (principal ${principal}, action == Sanoma::Action::"${action}", resource);`
|
|
148
|
+
);
|
|
149
|
+
};
|
|
83
150
|
|
|
84
151
|
/** A call as written, resolved as the app's `callApi` resolves it, with why it would fail. */
|
|
85
152
|
const resolve = ({ kind, name, via, ...at }: Written): { call: SiteCall; problem?: string } => {
|
|
@@ -110,12 +177,17 @@ export function readSites(
|
|
|
110
177
|
const file = readSource(path);
|
|
111
178
|
return typeof file === "string" ? [] : usesIn(file, relative(root, path).split(sep).join("/")).map(resolve);
|
|
112
179
|
});
|
|
113
|
-
const
|
|
180
|
+
const siteFile = `sites/${name}/site.ts`;
|
|
181
|
+
const { problems: optionProblems, ...options } = siteOptionsOf(join(root, siteFile), connectors);
|
|
114
182
|
const calls = resolved.map((r) => r.call);
|
|
115
|
-
const problems =
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
183
|
+
const problems = [
|
|
184
|
+
...resolved.flatMap(({ call, problem }) => {
|
|
185
|
+
const message = problem ?? (options.public ? refused(call, options.login) : undefined);
|
|
186
|
+
return message === undefined ? [] : [{ file: call.file, line: call.line, column: call.column, message }];
|
|
187
|
+
}),
|
|
188
|
+
...optionProblems.map((p) => ({ file: siteFile, ...p })),
|
|
189
|
+
];
|
|
190
|
+
return { name, ...options, calls, problems };
|
|
119
191
|
});
|
|
120
192
|
}
|
|
121
193
|
|
|
@@ -125,7 +197,7 @@ type Written = Omit<SiteCall, "target"> & { name: string };
|
|
|
125
197
|
/** The uses in one file, in its order. */
|
|
126
198
|
function usesIn({ program, at }: SourceFile, file: string): Written[] {
|
|
127
199
|
const api = importedAs(program, SITE_SOURCES, "api");
|
|
128
|
-
const runForm = importedAs(program,
|
|
200
|
+
const runForm = importedAs(program, SITE_SOURCES, "RunForm");
|
|
129
201
|
if (!api.size && !runForm.size) return [];
|
|
130
202
|
const found: Omit<Written, "page">[] = [];
|
|
131
203
|
let page: string | undefined;
|
|
@@ -156,7 +228,7 @@ function usesIn({ program, at }: SourceFile, file: string): Written[] {
|
|
|
156
228
|
}
|
|
157
229
|
|
|
158
230
|
/** What a member path on `api` names; undefined for a path on anything else, a name not written out, or no name. */
|
|
159
|
-
function apiPath(member: Node, api: ReadonlySet<string>): Pick<Written, "kind" | "name"> | undefined {
|
|
231
|
+
function apiPath(member: Node, api: ReadonlySet<string>): Pick<Written, "kind" | "name" | "method"> | undefined {
|
|
160
232
|
const path: string[] = [];
|
|
161
233
|
let node = member;
|
|
162
234
|
while (node?.type === "MemberExpression") {
|
|
@@ -166,10 +238,12 @@ function apiPath(member: Node, api: ReadonlySet<string>): Pick<Written, "kind" |
|
|
|
166
238
|
node = unwrap(node.object);
|
|
167
239
|
}
|
|
168
240
|
if (node?.type !== "Identifier" || !api.has(node.name)) return undefined;
|
|
169
|
-
const [first, second] = path as [string, ...string[]];
|
|
170
|
-
if (first === DATA_VENDOR)
|
|
241
|
+
const [first, second, third] = path as [string, ...string[]];
|
|
242
|
+
if (first === DATA_VENDOR)
|
|
243
|
+
return path.length === 3 ? { kind: "collection", name: second!, method: third } : undefined;
|
|
171
244
|
if (path.length === 3) return { kind: "op", name: path.join(".") };
|
|
172
|
-
|
|
245
|
+
if (path.length > 2) return undefined;
|
|
246
|
+
return { kind: "workflow", name: first, ...(second !== undefined && { method: second }) };
|
|
173
247
|
}
|
|
174
248
|
|
|
175
249
|
/** The local names `imported` has in the file, imported as a value from one of `sources`. */
|
|
@@ -187,22 +261,80 @@ function importedAs(program: Node, sources: ReadonlySet<string>, imported: strin
|
|
|
187
261
|
return names;
|
|
188
262
|
}
|
|
189
263
|
|
|
190
|
-
/**
|
|
191
|
-
|
|
264
|
+
/** The connectors a site's `login` is checked against: the config's vendors, and those with a login half. */
|
|
265
|
+
type SiteConnectors = { vendors: ReadonlySet<string>; logins: ReadonlySet<string> };
|
|
266
|
+
|
|
267
|
+
/** A property of an object literal that is written out: not a spread, its name not computed. */
|
|
268
|
+
const isWritten = (p: Node) => p.type === "Property" && !p.computed;
|
|
269
|
+
|
|
270
|
+
/** What `site.ts` that `sanoma` cannot read without running it is told. */
|
|
271
|
+
const unread = (what: string, as: string) => `${what} is read without running site.ts: write it as ${as}`;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* What `site.ts` says of its site, read without running it, and what is wrong there: its default
|
|
275
|
+
* export must be `defineSite({ … })` written out, so it can be read at all; then `title` when it is
|
|
276
|
+
* a string, `public` and `login`, which must be literals. `login` is kept as written; one on a
|
|
277
|
+
* site that is not public, or naming no connector of the config with a login half, is a problem.
|
|
278
|
+
*/
|
|
279
|
+
function siteOptionsOf(
|
|
280
|
+
path: string,
|
|
281
|
+
connectors: SiteConnectors,
|
|
282
|
+
): Pick<SiteEntry, "title" | "public" | "login"> & { problems: LintProblem[] } {
|
|
192
283
|
const file = readSource(path);
|
|
193
|
-
if (typeof file === "string") return
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const
|
|
284
|
+
if (typeof file === "string") return { problems: [] };
|
|
285
|
+
const problems: LintProblem[] = [...file.problems];
|
|
286
|
+
const problem = (at: Node, message: string) => void problems.push({ ...file.at(at.start), message });
|
|
287
|
+
const exported = file.program.body.find((s: Node) => s.type === "ExportDefaultDeclaration");
|
|
288
|
+
if (!exported) return { problems };
|
|
289
|
+
const call = unwrap(exported.declaration);
|
|
197
290
|
const callee = unwrap(call?.callee);
|
|
198
291
|
const options = unwrap(call?.arguments?.[0]);
|
|
199
|
-
|
|
292
|
+
const defineSite = importedAs(file.program, SITE_SOURCES, "defineSite");
|
|
293
|
+
// Anything not written out (a variable, a spread, a computed key) could make the site public unseen.
|
|
294
|
+
const written =
|
|
295
|
+
call?.type === "CallExpression" &&
|
|
296
|
+
callee?.type === "Identifier" &&
|
|
297
|
+
defineSite.has(callee.name) &&
|
|
298
|
+
(options === undefined || options.type === "ObjectExpression");
|
|
299
|
+
if (!written) {
|
|
300
|
+
problem(
|
|
301
|
+
exported,
|
|
302
|
+
unread("its default export", '`defineSite({ … })` from "sanoma/site", its options an object literal'),
|
|
303
|
+
);
|
|
304
|
+
return { problems };
|
|
305
|
+
}
|
|
306
|
+
for (const p of options?.properties ?? []) {
|
|
307
|
+
if (!isWritten(p)) problem(p, unread("each of `defineSite`'s options", "a property with its name"));
|
|
308
|
+
}
|
|
309
|
+
// The last of a name, as the object it makes has.
|
|
310
|
+
const property = (name: string): Node =>
|
|
311
|
+
options?.properties.findLast((p: Node) => isWritten(p) && keyName(p.key) === name);
|
|
312
|
+
|
|
313
|
+
const title = stringValue(property("title")?.value);
|
|
314
|
+
const publicAt = property("public");
|
|
315
|
+
const flag = unwrap(publicAt?.value);
|
|
316
|
+
const isPublic = flag?.type === "Literal" && flag.value === true;
|
|
317
|
+
const publicUnread = publicAt !== undefined && !(flag?.type === "Literal" && typeof flag.value === "boolean");
|
|
318
|
+
if (publicUnread) problem(publicAt, unread("`public`", "`true` or `false`"));
|
|
319
|
+
|
|
320
|
+
const loginAt = property("login");
|
|
321
|
+
const login = stringValue(loginAt?.value);
|
|
322
|
+
const loginProblem = (): string | undefined => {
|
|
323
|
+
if (login === undefined) return unread("`login`", "a string");
|
|
324
|
+
// When `public` is what is wrong, it is said there.
|
|
325
|
+
if (!isPublic && !publicUnread) return "a login is for a public site: add `public: true`, or remove `login`";
|
|
326
|
+
if (!connectors.vendors.has(login)) return `login names "${login}", which is not a connector in sanoma.config.ts`;
|
|
327
|
+
if (!connectors.logins.has(login)) return `login names "${login}", whose connector has no login half`;
|
|
200
328
|
return undefined;
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
329
|
+
};
|
|
330
|
+
const message = loginAt && loginProblem();
|
|
331
|
+
if (message) problem(loginAt, message);
|
|
332
|
+
return {
|
|
333
|
+
...(title !== undefined && { title }),
|
|
334
|
+
...(isPublic && { public: true as const }),
|
|
335
|
+
...(login !== undefined && { login }),
|
|
336
|
+
problems,
|
|
337
|
+
};
|
|
206
338
|
}
|
|
207
339
|
|
|
208
340
|
/**
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import type { VendorInfo } from "./op.ts";
|
|
6
|
+
import { upFrom } from "./paths.ts";
|
|
7
|
+
|
|
8
|
+
/** A logo file's path in the package: relative, never out of it (`..`), an SVG. */
|
|
9
|
+
const LogoFile = z.string().regex(/^(?!\/)(?!.*\.\.)[\w./-]+\.svg$/, "a relative path to an .svg file in the package");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The `sanoma` field of a connector's package.json: the vendor's name and its logo files, beside
|
|
13
|
+
* package.json. The app reads the same field from npm to list connectors not installed.
|
|
14
|
+
*/
|
|
15
|
+
export const SanomaPackageField = z.object({
|
|
16
|
+
title: z.string().min(1),
|
|
17
|
+
logo: LogoFile.optional(),
|
|
18
|
+
logoDark: LogoFile.optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A connector's `VendorInfo`, read from its own package.json, the one place it is written: the
|
|
23
|
+
* package's `name` and `homepage`, and the `sanoma` field's `title` and logo files, such as
|
|
24
|
+
* `"sanoma": { "title": "Ghost", "logo": "logo.svg", "logoDark": "logo-dark.svg" }`. The app
|
|
25
|
+
* lists connectors not installed from the same field on npm. `moduleUrl` is any module of the
|
|
26
|
+
* package, whose nearest package.json is read: `vendorInfo(import.meta.url)`.
|
|
27
|
+
*/
|
|
28
|
+
export function vendorInfo(moduleUrl: string | URL): VendorInfo {
|
|
29
|
+
const from = dirname(fileURLToPath(moduleUrl));
|
|
30
|
+
const root = upFrom(from, (dir) => (existsSync(join(dir, "package.json")) ? dir : undefined)) ?? from;
|
|
31
|
+
const read = (path: string) => readFileSync(join(root, path), "utf8").trim();
|
|
32
|
+
const pkg = JSON.parse(read("package.json")) as { name: string; homepage?: string; sanoma?: unknown };
|
|
33
|
+
const field = SanomaPackageField.safeParse(pkg.sanoma);
|
|
34
|
+
if (!field.success) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`${pkg.name}: package.json needs a "sanoma" field with a title, and logos as .svg paths in the package: ${z.prettifyError(field.error)}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const { title, logo, logoDark } = field.data;
|
|
40
|
+
const svg = (path: string) => {
|
|
41
|
+
try {
|
|
42
|
+
return read(path);
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error(`${pkg.name}: the "sanoma" field's logo ${path} is not in the package`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
title,
|
|
49
|
+
...(logo && { logo: { svg: svg(logo), ...(logoDark && { dark: svg(logoDark) }) } }),
|
|
50
|
+
package: pkg.name,
|
|
51
|
+
...(pkg.homepage && { homepage: pkg.homepage }),
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -5,8 +5,10 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import { jsonSchemaOf, opIdsOf, type WorkflowDefinition } from "./define.ts";
|
|
6
6
|
import { upFrom } from "./paths.ts";
|
|
7
7
|
|
|
8
|
-
/** This package's version.
|
|
9
|
-
export const RUNTIME_VERSION =
|
|
8
|
+
/** This package's version, from its package.json: one level above `src/` and `dist/` alike. */
|
|
9
|
+
export const RUNTIME_VERSION = (
|
|
10
|
+
JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { version: string }
|
|
11
|
+
).version;
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* The sequence of DBOS calls the runtime makes around each `ctx` call: policy step, approval
|