@shware/workflow 0.4.0 → 1.0.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 +48 -38
- package/dist/cloudflare/channels.cjs +12 -14
- package/dist/cloudflare/channels.cjs.map +1 -1
- package/dist/cloudflare/channels.d.cts.map +1 -1
- package/dist/cloudflare/channels.d.mts.map +1 -1
- package/dist/cloudflare/channels.mjs +13 -15
- package/dist/cloudflare/channels.mjs.map +1 -1
- package/dist/config.cjs +61 -36
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +19 -15
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +19 -15
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +61 -36
- package/dist/config.mjs.map +1 -1
- package/dist/dsl/action.cjs +1 -1
- package/dist/dsl/action.cjs.map +1 -1
- package/dist/dsl/action.d.cts +1 -1
- package/dist/dsl/action.d.mts +1 -1
- package/dist/dsl/action.mjs +1 -1
- package/dist/dsl/action.mjs.map +1 -1
- package/dist/dsl/bundle.cjs +3 -2
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts +3 -2
- package/dist/dsl/bundle.d.cts.map +1 -1
- package/dist/dsl/bundle.d.mts +3 -2
- package/dist/dsl/bundle.d.mts.map +1 -1
- package/dist/dsl/bundle.mjs +3 -2
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/condition.cjs +1 -1
- package/dist/dsl/condition.cjs.map +1 -1
- package/dist/dsl/condition.d.cts +1 -1
- package/dist/dsl/condition.d.mts +1 -1
- package/dist/dsl/condition.mjs +1 -1
- package/dist/dsl/condition.mjs.map +1 -1
- package/dist/dsl/flow.cjs +45 -17
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +30 -17
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +30 -17
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs +45 -18
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/index.cjs +0 -1
- package/dist/dsl/index.d.cts +2 -2
- package/dist/dsl/index.d.mts +2 -2
- package/dist/dsl/index.mjs +2 -2
- package/dist/dsl/template.cjs +2 -47
- package/dist/dsl/template.cjs.map +1 -1
- package/dist/dsl/template.d.cts +4 -54
- package/dist/dsl/template.d.cts.map +1 -1
- package/dist/dsl/template.d.mts +4 -54
- package/dist/dsl/template.d.mts.map +1 -1
- package/dist/dsl/template.mjs +3 -47
- package/dist/dsl/template.mjs.map +1 -1
- package/dist/dsl/workflow.cjs +1 -1
- package/dist/dsl/workflow.cjs.map +1 -1
- package/dist/dsl/workflow.d.cts +1 -1
- package/dist/dsl/workflow.d.mts +1 -1
- package/dist/dsl/workflow.mjs +1 -1
- package/dist/dsl/workflow.mjs.map +1 -1
- package/dist/index.cjs +5 -2
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -3
- package/dist/push.cjs +3 -5
- package/dist/push.cjs.map +1 -1
- package/dist/push.d.cts +2 -2
- package/dist/push.d.cts.map +1 -1
- package/dist/push.d.mts +2 -2
- package/dist/push.d.mts.map +1 -1
- package/dist/push.mjs +4 -6
- package/dist/push.mjs.map +1 -1
- package/dist/react-email.cjs +5 -5
- package/dist/react-email.cjs.map +1 -1
- package/dist/react-email.d.cts +1 -1
- package/dist/react-email.d.cts.map +1 -1
- package/dist/react-email.d.mts +1 -1
- package/dist/react-email.d.mts.map +1 -1
- package/dist/react-email.mjs +6 -6
- package/dist/react-email.mjs.map +1 -1
- package/dist/templates.cjs +84 -29
- package/dist/templates.cjs.map +1 -1
- package/dist/templates.d.cts +49 -11
- package/dist/templates.d.cts.map +1 -1
- package/dist/templates.d.mts +49 -11
- package/dist/templates.d.mts.map +1 -1
- package/dist/templates.mjs +81 -30
- package/dist/templates.mjs.map +1 -1
- package/package.json +1 -1
package/dist/dsl/template.d.mts
CHANGED
|
@@ -1,76 +1,26 @@
|
|
|
1
|
-
import { DiscordTemplate, EmailTemplate,
|
|
1
|
+
import { DiscordTemplate, EmailTemplate, PushTemplate, SlackTemplate } from "../templates.mjs";
|
|
2
2
|
import * as core from "zod/v4/core";
|
|
3
3
|
//#region src/dsl/template.d.ts
|
|
4
4
|
export type Channel = 'email' | 'sms' | 'push' | 'in_app' | 'slack' | 'discord' | 'survey';
|
|
5
5
|
export type EmptyProps = Record<never, never>;
|
|
6
|
-
/**
|
|
7
|
-
* Template reference: a top-level named asset, reusable across workflows and
|
|
8
|
-
* versioned on its own. The props shape is declared here and checked at use.
|
|
9
|
-
*/
|
|
6
|
+
/** A template by key, its props typed by hand: for the channels without template definitions. */
|
|
10
7
|
export interface TemplateRef<C extends Channel = Channel, P extends object = EmptyProps> {
|
|
11
8
|
readonly channel: C;
|
|
12
9
|
readonly key: string;
|
|
13
10
|
readonly __props?: P;
|
|
14
11
|
}
|
|
15
12
|
export interface TemplateFactory {
|
|
16
|
-
email<P extends object = EmptyProps>(key: string): TemplateRef<'email', P>;
|
|
17
13
|
sms<P extends object = EmptyProps>(key: string): TemplateRef<'sms', P>;
|
|
18
|
-
push<P extends object = EmptyProps>(key: string): TemplateRef<'push', P>;
|
|
19
14
|
inApp<P extends object = EmptyProps>(key: string): TemplateRef<'in_app', P>;
|
|
20
|
-
slack<P extends object = EmptyProps>(key: string): TemplateRef<'slack', P>;
|
|
21
|
-
discord<P extends object = EmptyProps>(key: string): TemplateRef<'discord', P>;
|
|
22
15
|
survey<P extends object = EmptyProps>(key: string): TemplateRef<'survey', P>;
|
|
23
16
|
}
|
|
24
17
|
/**
|
|
25
|
-
*
|
|
26
|
-
* from the registries (`templates<Emails, Pushes>()`), which type both key and
|
|
27
|
-
* props; this is the low-level form for channels without a registry (sms,
|
|
28
|
-
* slack, …) and for tests.
|
|
18
|
+
* Template references for the channels an app-supplied sender renders:
|
|
29
19
|
*
|
|
30
|
-
* const
|
|
20
|
+
* const code = template.sms<{ code: string }>('login_code');
|
|
31
21
|
*/
|
|
32
22
|
export declare const template: TemplateFactory;
|
|
33
23
|
/** Template props: the input type of the template's Zod `props` schema — what the DSL passes. */
|
|
34
24
|
export type PropsOf<M> = M extends EmailTemplate<infer S> | PushTemplate<infer S> | DiscordTemplate<infer S, never> | SlackTemplate<infer S, never> ? core.input<S> extends object ? core.input<S> : EmptyProps : never;
|
|
35
|
-
/**
|
|
36
|
-
* The channel registries: what the config registers as `templates` and what
|
|
37
|
-
* `templates()` derives the references from — one type for both. Leave it
|
|
38
|
-
* inferred, or check it with `satisfies TemplateRegistry`: annotating a value
|
|
39
|
-
* as the bare type widens the keys to `string`.
|
|
40
|
-
*/
|
|
41
|
-
export interface TemplateRegistry<E extends Readonly<Record<string, EmailTemplate>> = Readonly<Record<string, EmailTemplate>>, P extends Readonly<Record<string, PushTemplate>> = Readonly<Record<string, PushTemplate>>> {
|
|
42
|
-
readonly email?: E;
|
|
43
|
-
readonly push?: P;
|
|
44
|
-
/** Discord webhook templates, each typed by the host's Env its `webhook` reads. */
|
|
45
|
-
readonly discord?: Readonly<Record<string, DiscordTemplate<PropsSchema, never>>>;
|
|
46
|
-
readonly slack?: Readonly<Record<string, SlackTemplate<PropsSchema, never>>>;
|
|
47
|
-
}
|
|
48
|
-
/** One channel's registry in `R`, without the `undefined` of an optional field; `never` when absent. */
|
|
49
|
-
type ChannelRegistry<R, K extends 'email' | 'push' | 'discord' | 'slack'> = K extends keyof R ? Exclude<R[K], undefined> : never;
|
|
50
|
-
/** One reference per registered key, each typed by its own template's `props`. */
|
|
51
|
-
export type TemplateRefTable<C extends Channel, R> = { readonly [K in keyof R & string]: TemplateRef<C, PropsOf<R[K]>>; };
|
|
52
|
-
/** `t.email.<key>` / `t.push.<key>`; a channel the registry leaves out is an empty table. */
|
|
53
|
-
export interface TemplateRefs<R extends TemplateRegistry> {
|
|
54
|
-
readonly email: [ChannelRegistry<R, 'email'>] extends [never] ? EmptyProps : TemplateRefTable<'email', ChannelRegistry<R, 'email'>>;
|
|
55
|
-
readonly push: [ChannelRegistry<R, 'push'>] extends [never] ? EmptyProps : TemplateRefTable<'push', ChannelRegistry<R, 'push'>>;
|
|
56
|
-
readonly discord: [ChannelRegistry<R, 'discord'>] extends [never] ? EmptyProps : TemplateRefTable<'discord', ChannelRegistry<R, 'discord'>>;
|
|
57
|
-
readonly slack: [ChannelRegistry<R, 'slack'>] extends [never] ? EmptyProps : TemplateRefTable<'slack', ChannelRegistry<R, 'slack'>>;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Template references derived from the registries themselves: **the
|
|
61
|
-
* registries type the keys** — an unregistered key fails to compile, and a
|
|
62
|
-
* push template is not accepted where an email is — and each template's
|
|
63
|
-
* `props` schema types its use sites. Templates are registered once; the
|
|
64
|
-
* config takes the registries, the workflows take the references:
|
|
65
|
-
*
|
|
66
|
-
* export const registry = { email, push };
|
|
67
|
-
* export const t = templates(registry);
|
|
68
|
-
* flow.email(t.email.welcome, { name: u.name });
|
|
69
|
-
*
|
|
70
|
-
* Each reference is a plain `{ channel, key }` built from the registry's own
|
|
71
|
-
* keys; no template is rendered, and the registries are left untouched.
|
|
72
|
-
*/
|
|
73
|
-
export declare function templates<const R extends TemplateRegistry>(registry: R): TemplateRefs<R>;
|
|
74
25
|
//#endregion
|
|
75
|
-
export {};
|
|
76
26
|
//# sourceMappingURL=template.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.d.mts","names":[],"sources":["../../src/dsl/template.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"template.d.mts","names":[],"sources":["../../src/dsl/template.ts"],"mappings":";;;YAUY;YAEA,aAAa;;iBAGR,YAAY,UAAU,UAAU,SAAS,mBAAmB;WAClE,SAAS;WACT;WACA,UAAU;;iBAGJ;EACf,IAAI,mBAAmB,YAAY,cAAc,mBAAmB;EACpE,MAAM,mBAAmB,YAAY,cAAc,sBAAsB;EACzE,OAAO,mBAAmB,YAAY,cAAc,sBAAsB;;;;;;;qBAY/D,UAAU;;YAOX,QAAQ,KAAK,UACrB,oBAAoB,KACpB,mBAAmB,KACnB,sBAAsB,YACtB,oBAAoB,YACpB,KAAK,MAAM,oBACT,KAAK,MAAM,KACX"}
|
package/dist/dsl/template.mjs
CHANGED
|
@@ -1,63 +1,19 @@
|
|
|
1
|
-
import { isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate } from "../templates.mjs";
|
|
2
1
|
//#region src/dsl/template.ts
|
|
3
2
|
const makeTemplate = (channel) => (key) => ({
|
|
4
3
|
channel,
|
|
5
4
|
key
|
|
6
5
|
});
|
|
7
6
|
/**
|
|
8
|
-
*
|
|
9
|
-
* from the registries (`templates<Emails, Pushes>()`), which type both key and
|
|
10
|
-
* props; this is the low-level form for channels without a registry (sms,
|
|
11
|
-
* slack, …) and for tests.
|
|
7
|
+
* Template references for the channels an app-supplied sender renders:
|
|
12
8
|
*
|
|
13
|
-
* const
|
|
9
|
+
* const code = template.sms<{ code: string }>('login_code');
|
|
14
10
|
*/
|
|
15
11
|
const template = {
|
|
16
|
-
email: makeTemplate("email"),
|
|
17
12
|
sms: makeTemplate("sms"),
|
|
18
|
-
push: makeTemplate("push"),
|
|
19
13
|
inApp: makeTemplate("in_app"),
|
|
20
|
-
slack: makeTemplate("slack"),
|
|
21
|
-
discord: makeTemplate("discord"),
|
|
22
14
|
survey: makeTemplate("survey")
|
|
23
15
|
};
|
|
24
|
-
/**
|
|
25
|
-
* Template references derived from the registries themselves: **the
|
|
26
|
-
* registries type the keys** — an unregistered key fails to compile, and a
|
|
27
|
-
* push template is not accepted where an email is — and each template's
|
|
28
|
-
* `props` schema types its use sites. Templates are registered once; the
|
|
29
|
-
* config takes the registries, the workflows take the references:
|
|
30
|
-
*
|
|
31
|
-
* export const registry = { email, push };
|
|
32
|
-
* export const t = templates(registry);
|
|
33
|
-
* flow.email(t.email.welcome, { name: u.name });
|
|
34
|
-
*
|
|
35
|
-
* Each reference is a plain `{ channel, key }` built from the registry's own
|
|
36
|
-
* keys; no template is rendered, and the registries are left untouched.
|
|
37
|
-
*/
|
|
38
|
-
function templates(registry) {
|
|
39
|
-
return {
|
|
40
|
-
email: refTable("email", registry.email, isEmailTemplate),
|
|
41
|
-
push: refTable("push", registry.push, isPushTemplate),
|
|
42
|
-
discord: refTable("discord", registry.discord, isDiscordTemplate),
|
|
43
|
-
slack: refTable("slack", registry.slack, isSlackTemplate)
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
const ARTICLE = {
|
|
47
|
-
email: "an email",
|
|
48
|
-
push: "a push",
|
|
49
|
-
discord: "a Discord",
|
|
50
|
-
slack: "a Slack"
|
|
51
|
-
};
|
|
52
|
-
function refTable(channel, entries, is) {
|
|
53
|
-
const table = Object.create(null);
|
|
54
|
-
for (const [key, entry] of Object.entries(entries ?? {})) {
|
|
55
|
-
if (!is(entry)) throw new TypeError(`templates(): '${key}' in the ${channel} registry is not ${ARTICLE[channel]} template`);
|
|
56
|
-
table[key] = template[channel](key);
|
|
57
|
-
}
|
|
58
|
-
return Object.freeze(table);
|
|
59
|
-
}
|
|
60
16
|
//#endregion
|
|
61
|
-
export { template
|
|
17
|
+
export { template };
|
|
62
18
|
|
|
63
19
|
//# sourceMappingURL=template.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.mjs","names":[],"sources":["../../src/dsl/template.ts"],"sourcesContent":["/**\n * Templates
|
|
1
|
+
{"version":3,"file":"template.mjs","names":[],"sources":["../../src/dsl/template.ts"],"sourcesContent":["/**\n * Templates in the DSL. The four channels with template definitions (email, push, discord,\n * slack) take the definition itself — `.email(welcome, props)` — so its Zod `props` schema types\n * the call (`PropsOf`) and its `id` lands in the IR. The channels rendered by app-supplied\n * senders (sms, in-app, survey) have no definition; they take a hand-typed reference by key.\n */\n\nimport type * as core from 'zod/v4/core';\nimport type { DiscordTemplate, EmailTemplate, PushTemplate, SlackTemplate } from '../templates';\n\nexport type Channel = 'email' | 'sms' | 'push' | 'in_app' | 'slack' | 'discord' | 'survey';\n\nexport type EmptyProps = Record<never, never>;\n\n/** A template by key, its props typed by hand: for the channels without template definitions. */\nexport interface TemplateRef<C extends Channel = Channel, P extends object = EmptyProps> {\n readonly channel: C;\n readonly key: string;\n readonly __props?: P;\n}\n\nexport interface TemplateFactory {\n sms<P extends object = EmptyProps>(key: string): TemplateRef<'sms', P>;\n inApp<P extends object = EmptyProps>(key: string): TemplateRef<'in_app', P>;\n survey<P extends object = EmptyProps>(key: string): TemplateRef<'survey', P>;\n}\n\nconst makeTemplate =\n <C extends Channel>(channel: C) =>\n <P extends object = EmptyProps>(key: string): TemplateRef<C, P> => ({ channel, key });\n\n/**\n * Template references for the channels an app-supplied sender renders:\n *\n * const code = template.sms<{ code: string }>('login_code');\n */\nexport const template: TemplateFactory = {\n sms: makeTemplate('sms'),\n inApp: makeTemplate('in_app'),\n survey: makeTemplate('survey'),\n};\n\n/** Template props: the input type of the template's Zod `props` schema — what the DSL passes. */\nexport type PropsOf<M> = M extends\n | EmailTemplate<infer S>\n | PushTemplate<infer S>\n | DiscordTemplate<infer S, never>\n | SlackTemplate<infer S, never>\n ? core.input<S> extends object\n ? core.input<S>\n : EmptyProps\n : never;\n"],"mappings":";AA2BA,MAAM,gBACgB,aACY,SAAoC;CAAE;CAAS;AAAI;;;;;;AAOrF,MAAa,WAA4B;CACvC,KAAK,aAAa,KAAK;CACvB,OAAO,aAAa,QAAQ;CAC5B,QAAQ,aAAa,QAAQ;AAC/B"}
|
package/dist/dsl/workflow.cjs
CHANGED
|
@@ -110,7 +110,7 @@ var WorkflowBuilderImpl = class extends require_dsl_flow.FlowBuilderImpl {
|
|
|
110
110
|
* id: 'onboarding',
|
|
111
111
|
* name: 'Onboarding',
|
|
112
112
|
* trigger: trigger.event(e.sign_up),
|
|
113
|
-
* }).email(
|
|
113
|
+
* }).email(email.welcome);
|
|
114
114
|
*/
|
|
115
115
|
function workflow(definition) {
|
|
116
116
|
return new WorkflowBuilderImpl(definition, require_provenance.captureLoc(workflow));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflow.cjs","names":["FlowBuilderImpl","condIR","durationIR","WorkflowIRSchema","semanticHash","captureLoc"],"sources":["../../src/dsl/workflow.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type GoalIR,\n IR_VERSION,\n type MetaIR,\n type NodeIR,\n type SourceLocIR,\n type WorkflowIR,\n WorkflowIR as WorkflowIRSchema,\n} from '../ir';\nimport { captureLoc } from '../provenance';\nimport { type Duration, durationIR } from './base';\nimport { type Condition, condIR } from './condition';\nimport { type FlowBuilder, FlowBuilderImpl } from './flow';\nimport type { TriggerInternal, TriggerRef } from './trigger';\n\n/**\n * The workflow — a named flow chain plus its configuration (options =\n * configuration, the chain = steps), and the compiler back half: `toIR()`\n * deep-clones the collected nodes, assigns structural-path ids, computes the\n * contentHash (metadata stripped, see hash.ts) and self-checks the result\n * against the authoritative IR schema.\n */\n\n/** Full conversion-goal configuration (customer.io's Goal & Exit semantics). */\nexport interface GoalOptions {\n condition: Condition;\n /** Attribution window: how long after entry a match still counts as a conversion. Unbounded by default. */\n within?: Duration;\n /** Exit on conversion. Defaults to true — this is what absorbs all those True→Exit arms in a UI canvas. */\n exitOnMatch?: boolean;\n}\n\nexport interface WorkflowDefinition {\n /**\n * Wire identity — engine instances, the entry ledger and stats keys are all\n * built on it (`WorkflowIR.id`), so it is not something to rename for\n * readability; `name` is.\n */\n id: string;\n\n /** Trigger: a trigger.xxx() asset. TODO: multiple triggers, entry frequency / re-entry policy. */\n trigger: TriggerRef;\n\n /**\n * Conversion goal: feeds reporting (conversion rate / attribution) and by\n * default exits on match. Checked before each node runs. Pass a condition\n * for the shorthand, or GoalOptions for the full form.\n */\n goal?: Condition | GoalOptions;\n\n /** Plain exit condition: leaving without counting as a conversion (unsubscribed, no longer eligible). Coexists with goal. */\n exitWhen?: Condition;\n\n /*\n * Everything below is human-facing metadata: it lands in IR's `meta` field\n * and is **excluded from contentHash**. Editing it neither invalidates the\n * version in-flight users are pinned to nor shows up as a change in plan.\n */\n\n /** Human label: what the studio shows instead of the id. */\n name?: string;\n\n /** One line on what this flow does (UI lists, plan output). */\n description?: string;\n}\n\nexport interface WorkflowBuilder extends FlowBuilder {\n toIR(): WorkflowIR;\n}\n\n/**\n * Assign node ids from the structural path (rules in ir.ts's header). Written\n * in place into the tree toIR deep-cloned, never into the builder's own nodes.\n */\nfunction assignIds(nodes: NodeIR[], prefix: string): void {\n nodes.forEach((node, i) => {\n const id = prefix === '' ? String(i) : `${prefix}.${i}`;\n node.id = id;\n switch (node.type) {\n case 'branch':\n node.cases.forEach((c, ci) => assignIds(c.flow, `${id}.c${ci}`));\n if (node.otherwise) assignIds(node.otherwise, `${id}.o`);\n break;\n case 'cohort':\n node.arms.forEach((arm) => assignIds(arm.flow, `${id}.${arm.name}`));\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assignIds(node.onTimeout, `${id}.t`);\n break;\n default:\n break;\n }\n });\n}\n\n/**\n * Ids are derived, never authored, so a duplicate is always a compiler bug —\n * and a silent one: assignIds writes in place, so the second write simply wins\n * and two nodes ship as one. What that costs is concrete, because an id is the\n * durable step name (`step.do('{id}:send')`), the message idempotency key\n * (`{instanceId}:{id}`) and the cohort bucket seed: the twins share a\n * checkpoint, and the second send is suppressed as a duplicate of the first.\n * Cheap to check once at the end of the compile, so it is checked.\n */\nfunction assertUniqueIds(nodes: readonly NodeIR[], seen = new Set<string>()): void {\n for (const node of nodes) {\n if (seen.has(node.id)) {\n throw new Error(\n `toIR(): node id '${node.id}' is used twice — ids are durable step names and message idempotency keys, so they must be unique`\n );\n }\n seen.add(node.id);\n switch (node.type) {\n case 'branch':\n for (const branchCase of node.cases) assertUniqueIds(branchCase.flow, seen);\n if (node.otherwise) assertUniqueIds(node.otherwise, seen);\n break;\n case 'cohort':\n for (const arm of node.arms) assertUniqueIds(arm.flow, seen);\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assertUniqueIds(node.onTimeout, seen);\n break;\n default:\n break;\n }\n }\n}\n\nclass WorkflowBuilderImpl extends FlowBuilderImpl implements WorkflowBuilder {\n constructor(\n private readonly options: WorkflowDefinition,\n /** Callsite of the workflow() call (provenance; lands in meta.loc). */\n private readonly loc: SourceLocIR | undefined\n ) {\n super();\n }\n\n private goalIR(): GoalIR | undefined {\n const goal = this.options.goal;\n if (goal === undefined) return undefined;\n if ('__condition' in goal) {\n return { condition: condIR(goal), exitOnMatch: true };\n }\n return {\n condition: condIR(goal.condition),\n ...(goal.within !== undefined ? { within: durationIR(goal.within) } : {}),\n exitOnMatch: goal.exitOnMatch ?? true,\n };\n }\n\n private metaIR(): MetaIR | undefined {\n const { name, description } = this.options;\n if (name === undefined && description === undefined && this.loc === undefined) {\n return undefined;\n }\n return {\n ...(name !== undefined ? { name } : {}),\n ...(description !== undefined ? { description } : {}),\n ...(this.loc !== undefined ? { loc: this.loc } : {}),\n };\n }\n\n toIR(): WorkflowIR {\n const flowNodes = structuredClone(this.nodes);\n assignIds(flowNodes, '');\n assertUniqueIds(flowNodes);\n const goal = this.goalIR();\n const meta = this.metaIR();\n const body = {\n irVersion: IR_VERSION,\n id: this.options.id,\n ...(meta !== undefined ? { meta } : {}),\n trigger: (this.options.trigger as TriggerInternal).ir,\n ...(goal !== undefined ? { goal } : {}),\n ...(this.options.exitWhen !== undefined ? { exitWhen: condIR(this.options.exitWhen) } : {}),\n flow: flowNodes,\n };\n // semanticHash strips meta / label / reason, so metadata edits leave contentHash alone\n // Self-check: the compiler's output must pass IR's authoritative schema\n return WorkflowIRSchema.parse({ ...body, contentHash: semanticHash(body) });\n }\n}\n\n/**\n * Workflow definition: an id, configuration (trigger / goal / exitWhen),\n * optional labels, and chained steps.\n *\n * export const onboarding = workflow({\n * id: 'onboarding',\n * name: 'Onboarding',\n * trigger: trigger.event(e.sign_up),\n * }).email(
|
|
1
|
+
{"version":3,"file":"workflow.cjs","names":["FlowBuilderImpl","condIR","durationIR","WorkflowIRSchema","semanticHash","captureLoc"],"sources":["../../src/dsl/workflow.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type GoalIR,\n IR_VERSION,\n type MetaIR,\n type NodeIR,\n type SourceLocIR,\n type WorkflowIR,\n WorkflowIR as WorkflowIRSchema,\n} from '../ir';\nimport { captureLoc } from '../provenance';\nimport { type Duration, durationIR } from './base';\nimport { type Condition, condIR } from './condition';\nimport { type FlowBuilder, FlowBuilderImpl } from './flow';\nimport type { TriggerInternal, TriggerRef } from './trigger';\n\n/**\n * The workflow — a named flow chain plus its configuration (options =\n * configuration, the chain = steps), and the compiler back half: `toIR()`\n * deep-clones the collected nodes, assigns structural-path ids, computes the\n * contentHash (metadata stripped, see hash.ts) and self-checks the result\n * against the authoritative IR schema.\n */\n\n/** Full conversion-goal configuration (customer.io's Goal & Exit semantics). */\nexport interface GoalOptions {\n condition: Condition;\n /** Attribution window: how long after entry a match still counts as a conversion. Unbounded by default. */\n within?: Duration;\n /** Exit on conversion. Defaults to true — this is what absorbs all those True→Exit arms in a UI canvas. */\n exitOnMatch?: boolean;\n}\n\nexport interface WorkflowDefinition {\n /**\n * Wire identity — engine instances, the entry ledger and stats keys are all\n * built on it (`WorkflowIR.id`), so it is not something to rename for\n * readability; `name` is.\n */\n id: string;\n\n /** Trigger: a trigger.xxx() asset. TODO: multiple triggers, entry frequency / re-entry policy. */\n trigger: TriggerRef;\n\n /**\n * Conversion goal: feeds reporting (conversion rate / attribution) and by\n * default exits on match. Checked before each node runs. Pass a condition\n * for the shorthand, or GoalOptions for the full form.\n */\n goal?: Condition | GoalOptions;\n\n /** Plain exit condition: leaving without counting as a conversion (unsubscribed, no longer eligible). Coexists with goal. */\n exitWhen?: Condition;\n\n /*\n * Everything below is human-facing metadata: it lands in IR's `meta` field\n * and is **excluded from contentHash**. Editing it neither invalidates the\n * version in-flight users are pinned to nor shows up as a change in plan.\n */\n\n /** Human label: what the studio shows instead of the id. */\n name?: string;\n\n /** One line on what this flow does (UI lists, plan output). */\n description?: string;\n}\n\nexport interface WorkflowBuilder extends FlowBuilder {\n toIR(): WorkflowIR;\n}\n\n/**\n * Assign node ids from the structural path (rules in ir.ts's header). Written\n * in place into the tree toIR deep-cloned, never into the builder's own nodes.\n */\nfunction assignIds(nodes: NodeIR[], prefix: string): void {\n nodes.forEach((node, i) => {\n const id = prefix === '' ? String(i) : `${prefix}.${i}`;\n node.id = id;\n switch (node.type) {\n case 'branch':\n node.cases.forEach((c, ci) => assignIds(c.flow, `${id}.c${ci}`));\n if (node.otherwise) assignIds(node.otherwise, `${id}.o`);\n break;\n case 'cohort':\n node.arms.forEach((arm) => assignIds(arm.flow, `${id}.${arm.name}`));\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assignIds(node.onTimeout, `${id}.t`);\n break;\n default:\n break;\n }\n });\n}\n\n/**\n * Ids are derived, never authored, so a duplicate is always a compiler bug —\n * and a silent one: assignIds writes in place, so the second write simply wins\n * and two nodes ship as one. What that costs is concrete, because an id is the\n * durable step name (`step.do('{id}:send')`), the message idempotency key\n * (`{instanceId}:{id}`) and the cohort bucket seed: the twins share a\n * checkpoint, and the second send is suppressed as a duplicate of the first.\n * Cheap to check once at the end of the compile, so it is checked.\n */\nfunction assertUniqueIds(nodes: readonly NodeIR[], seen = new Set<string>()): void {\n for (const node of nodes) {\n if (seen.has(node.id)) {\n throw new Error(\n `toIR(): node id '${node.id}' is used twice — ids are durable step names and message idempotency keys, so they must be unique`\n );\n }\n seen.add(node.id);\n switch (node.type) {\n case 'branch':\n for (const branchCase of node.cases) assertUniqueIds(branchCase.flow, seen);\n if (node.otherwise) assertUniqueIds(node.otherwise, seen);\n break;\n case 'cohort':\n for (const arm of node.arms) assertUniqueIds(arm.flow, seen);\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assertUniqueIds(node.onTimeout, seen);\n break;\n default:\n break;\n }\n }\n}\n\nclass WorkflowBuilderImpl extends FlowBuilderImpl implements WorkflowBuilder {\n constructor(\n private readonly options: WorkflowDefinition,\n /** Callsite of the workflow() call (provenance; lands in meta.loc). */\n private readonly loc: SourceLocIR | undefined\n ) {\n super();\n }\n\n private goalIR(): GoalIR | undefined {\n const goal = this.options.goal;\n if (goal === undefined) return undefined;\n if ('__condition' in goal) {\n return { condition: condIR(goal), exitOnMatch: true };\n }\n return {\n condition: condIR(goal.condition),\n ...(goal.within !== undefined ? { within: durationIR(goal.within) } : {}),\n exitOnMatch: goal.exitOnMatch ?? true,\n };\n }\n\n private metaIR(): MetaIR | undefined {\n const { name, description } = this.options;\n if (name === undefined && description === undefined && this.loc === undefined) {\n return undefined;\n }\n return {\n ...(name !== undefined ? { name } : {}),\n ...(description !== undefined ? { description } : {}),\n ...(this.loc !== undefined ? { loc: this.loc } : {}),\n };\n }\n\n toIR(): WorkflowIR {\n const flowNodes = structuredClone(this.nodes);\n assignIds(flowNodes, '');\n assertUniqueIds(flowNodes);\n const goal = this.goalIR();\n const meta = this.metaIR();\n const body = {\n irVersion: IR_VERSION,\n id: this.options.id,\n ...(meta !== undefined ? { meta } : {}),\n trigger: (this.options.trigger as TriggerInternal).ir,\n ...(goal !== undefined ? { goal } : {}),\n ...(this.options.exitWhen !== undefined ? { exitWhen: condIR(this.options.exitWhen) } : {}),\n flow: flowNodes,\n };\n // semanticHash strips meta / label / reason, so metadata edits leave contentHash alone\n // Self-check: the compiler's output must pass IR's authoritative schema\n return WorkflowIRSchema.parse({ ...body, contentHash: semanticHash(body) });\n }\n}\n\n/**\n * Workflow definition: an id, configuration (trigger / goal / exitWhen),\n * optional labels, and chained steps.\n *\n * export const onboarding = workflow({\n * id: 'onboarding',\n * name: 'Onboarding',\n * trigger: trigger.event(e.sign_up),\n * }).email(email.welcome);\n */\nexport function workflow(definition: WorkflowDefinition): WorkflowBuilder {\n return new WorkflowBuilderImpl(definition, captureLoc(workflow));\n}\n"],"mappings":"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"}
|
package/dist/dsl/workflow.d.cts
CHANGED
|
@@ -52,7 +52,7 @@ export interface WorkflowBuilder extends FlowBuilder {
|
|
|
52
52
|
* id: 'onboarding',
|
|
53
53
|
* name: 'Onboarding',
|
|
54
54
|
* trigger: trigger.event(e.sign_up),
|
|
55
|
-
* }).email(
|
|
55
|
+
* }).email(email.welcome);
|
|
56
56
|
*/
|
|
57
57
|
export declare function workflow(definition: WorkflowDefinition): WorkflowBuilder;
|
|
58
58
|
//#endregion
|
package/dist/dsl/workflow.d.mts
CHANGED
|
@@ -52,7 +52,7 @@ export interface WorkflowBuilder extends FlowBuilder {
|
|
|
52
52
|
* id: 'onboarding',
|
|
53
53
|
* name: 'Onboarding',
|
|
54
54
|
* trigger: trigger.event(e.sign_up),
|
|
55
|
-
* }).email(
|
|
55
|
+
* }).email(email.welcome);
|
|
56
56
|
*/
|
|
57
57
|
export declare function workflow(definition: WorkflowDefinition): WorkflowBuilder;
|
|
58
58
|
//#endregion
|
package/dist/dsl/workflow.mjs
CHANGED
|
@@ -109,7 +109,7 @@ var WorkflowBuilderImpl = class extends FlowBuilderImpl {
|
|
|
109
109
|
* id: 'onboarding',
|
|
110
110
|
* name: 'Onboarding',
|
|
111
111
|
* trigger: trigger.event(e.sign_up),
|
|
112
|
-
* }).email(
|
|
112
|
+
* }).email(email.welcome);
|
|
113
113
|
*/
|
|
114
114
|
function workflow(definition) {
|
|
115
115
|
return new WorkflowBuilderImpl(definition, captureLoc(workflow));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflow.mjs","names":["WorkflowIRSchema"],"sources":["../../src/dsl/workflow.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type GoalIR,\n IR_VERSION,\n type MetaIR,\n type NodeIR,\n type SourceLocIR,\n type WorkflowIR,\n WorkflowIR as WorkflowIRSchema,\n} from '../ir';\nimport { captureLoc } from '../provenance';\nimport { type Duration, durationIR } from './base';\nimport { type Condition, condIR } from './condition';\nimport { type FlowBuilder, FlowBuilderImpl } from './flow';\nimport type { TriggerInternal, TriggerRef } from './trigger';\n\n/**\n * The workflow — a named flow chain plus its configuration (options =\n * configuration, the chain = steps), and the compiler back half: `toIR()`\n * deep-clones the collected nodes, assigns structural-path ids, computes the\n * contentHash (metadata stripped, see hash.ts) and self-checks the result\n * against the authoritative IR schema.\n */\n\n/** Full conversion-goal configuration (customer.io's Goal & Exit semantics). */\nexport interface GoalOptions {\n condition: Condition;\n /** Attribution window: how long after entry a match still counts as a conversion. Unbounded by default. */\n within?: Duration;\n /** Exit on conversion. Defaults to true — this is what absorbs all those True→Exit arms in a UI canvas. */\n exitOnMatch?: boolean;\n}\n\nexport interface WorkflowDefinition {\n /**\n * Wire identity — engine instances, the entry ledger and stats keys are all\n * built on it (`WorkflowIR.id`), so it is not something to rename for\n * readability; `name` is.\n */\n id: string;\n\n /** Trigger: a trigger.xxx() asset. TODO: multiple triggers, entry frequency / re-entry policy. */\n trigger: TriggerRef;\n\n /**\n * Conversion goal: feeds reporting (conversion rate / attribution) and by\n * default exits on match. Checked before each node runs. Pass a condition\n * for the shorthand, or GoalOptions for the full form.\n */\n goal?: Condition | GoalOptions;\n\n /** Plain exit condition: leaving without counting as a conversion (unsubscribed, no longer eligible). Coexists with goal. */\n exitWhen?: Condition;\n\n /*\n * Everything below is human-facing metadata: it lands in IR's `meta` field\n * and is **excluded from contentHash**. Editing it neither invalidates the\n * version in-flight users are pinned to nor shows up as a change in plan.\n */\n\n /** Human label: what the studio shows instead of the id. */\n name?: string;\n\n /** One line on what this flow does (UI lists, plan output). */\n description?: string;\n}\n\nexport interface WorkflowBuilder extends FlowBuilder {\n toIR(): WorkflowIR;\n}\n\n/**\n * Assign node ids from the structural path (rules in ir.ts's header). Written\n * in place into the tree toIR deep-cloned, never into the builder's own nodes.\n */\nfunction assignIds(nodes: NodeIR[], prefix: string): void {\n nodes.forEach((node, i) => {\n const id = prefix === '' ? String(i) : `${prefix}.${i}`;\n node.id = id;\n switch (node.type) {\n case 'branch':\n node.cases.forEach((c, ci) => assignIds(c.flow, `${id}.c${ci}`));\n if (node.otherwise) assignIds(node.otherwise, `${id}.o`);\n break;\n case 'cohort':\n node.arms.forEach((arm) => assignIds(arm.flow, `${id}.${arm.name}`));\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assignIds(node.onTimeout, `${id}.t`);\n break;\n default:\n break;\n }\n });\n}\n\n/**\n * Ids are derived, never authored, so a duplicate is always a compiler bug —\n * and a silent one: assignIds writes in place, so the second write simply wins\n * and two nodes ship as one. What that costs is concrete, because an id is the\n * durable step name (`step.do('{id}:send')`), the message idempotency key\n * (`{instanceId}:{id}`) and the cohort bucket seed: the twins share a\n * checkpoint, and the second send is suppressed as a duplicate of the first.\n * Cheap to check once at the end of the compile, so it is checked.\n */\nfunction assertUniqueIds(nodes: readonly NodeIR[], seen = new Set<string>()): void {\n for (const node of nodes) {\n if (seen.has(node.id)) {\n throw new Error(\n `toIR(): node id '${node.id}' is used twice — ids are durable step names and message idempotency keys, so they must be unique`\n );\n }\n seen.add(node.id);\n switch (node.type) {\n case 'branch':\n for (const branchCase of node.cases) assertUniqueIds(branchCase.flow, seen);\n if (node.otherwise) assertUniqueIds(node.otherwise, seen);\n break;\n case 'cohort':\n for (const arm of node.arms) assertUniqueIds(arm.flow, seen);\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assertUniqueIds(node.onTimeout, seen);\n break;\n default:\n break;\n }\n }\n}\n\nclass WorkflowBuilderImpl extends FlowBuilderImpl implements WorkflowBuilder {\n constructor(\n private readonly options: WorkflowDefinition,\n /** Callsite of the workflow() call (provenance; lands in meta.loc). */\n private readonly loc: SourceLocIR | undefined\n ) {\n super();\n }\n\n private goalIR(): GoalIR | undefined {\n const goal = this.options.goal;\n if (goal === undefined) return undefined;\n if ('__condition' in goal) {\n return { condition: condIR(goal), exitOnMatch: true };\n }\n return {\n condition: condIR(goal.condition),\n ...(goal.within !== undefined ? { within: durationIR(goal.within) } : {}),\n exitOnMatch: goal.exitOnMatch ?? true,\n };\n }\n\n private metaIR(): MetaIR | undefined {\n const { name, description } = this.options;\n if (name === undefined && description === undefined && this.loc === undefined) {\n return undefined;\n }\n return {\n ...(name !== undefined ? { name } : {}),\n ...(description !== undefined ? { description } : {}),\n ...(this.loc !== undefined ? { loc: this.loc } : {}),\n };\n }\n\n toIR(): WorkflowIR {\n const flowNodes = structuredClone(this.nodes);\n assignIds(flowNodes, '');\n assertUniqueIds(flowNodes);\n const goal = this.goalIR();\n const meta = this.metaIR();\n const body = {\n irVersion: IR_VERSION,\n id: this.options.id,\n ...(meta !== undefined ? { meta } : {}),\n trigger: (this.options.trigger as TriggerInternal).ir,\n ...(goal !== undefined ? { goal } : {}),\n ...(this.options.exitWhen !== undefined ? { exitWhen: condIR(this.options.exitWhen) } : {}),\n flow: flowNodes,\n };\n // semanticHash strips meta / label / reason, so metadata edits leave contentHash alone\n // Self-check: the compiler's output must pass IR's authoritative schema\n return WorkflowIRSchema.parse({ ...body, contentHash: semanticHash(body) });\n }\n}\n\n/**\n * Workflow definition: an id, configuration (trigger / goal / exitWhen),\n * optional labels, and chained steps.\n *\n * export const onboarding = workflow({\n * id: 'onboarding',\n * name: 'Onboarding',\n * trigger: trigger.event(e.sign_up),\n * }).email(
|
|
1
|
+
{"version":3,"file":"workflow.mjs","names":["WorkflowIRSchema"],"sources":["../../src/dsl/workflow.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type GoalIR,\n IR_VERSION,\n type MetaIR,\n type NodeIR,\n type SourceLocIR,\n type WorkflowIR,\n WorkflowIR as WorkflowIRSchema,\n} from '../ir';\nimport { captureLoc } from '../provenance';\nimport { type Duration, durationIR } from './base';\nimport { type Condition, condIR } from './condition';\nimport { type FlowBuilder, FlowBuilderImpl } from './flow';\nimport type { TriggerInternal, TriggerRef } from './trigger';\n\n/**\n * The workflow — a named flow chain plus its configuration (options =\n * configuration, the chain = steps), and the compiler back half: `toIR()`\n * deep-clones the collected nodes, assigns structural-path ids, computes the\n * contentHash (metadata stripped, see hash.ts) and self-checks the result\n * against the authoritative IR schema.\n */\n\n/** Full conversion-goal configuration (customer.io's Goal & Exit semantics). */\nexport interface GoalOptions {\n condition: Condition;\n /** Attribution window: how long after entry a match still counts as a conversion. Unbounded by default. */\n within?: Duration;\n /** Exit on conversion. Defaults to true — this is what absorbs all those True→Exit arms in a UI canvas. */\n exitOnMatch?: boolean;\n}\n\nexport interface WorkflowDefinition {\n /**\n * Wire identity — engine instances, the entry ledger and stats keys are all\n * built on it (`WorkflowIR.id`), so it is not something to rename for\n * readability; `name` is.\n */\n id: string;\n\n /** Trigger: a trigger.xxx() asset. TODO: multiple triggers, entry frequency / re-entry policy. */\n trigger: TriggerRef;\n\n /**\n * Conversion goal: feeds reporting (conversion rate / attribution) and by\n * default exits on match. Checked before each node runs. Pass a condition\n * for the shorthand, or GoalOptions for the full form.\n */\n goal?: Condition | GoalOptions;\n\n /** Plain exit condition: leaving without counting as a conversion (unsubscribed, no longer eligible). Coexists with goal. */\n exitWhen?: Condition;\n\n /*\n * Everything below is human-facing metadata: it lands in IR's `meta` field\n * and is **excluded from contentHash**. Editing it neither invalidates the\n * version in-flight users are pinned to nor shows up as a change in plan.\n */\n\n /** Human label: what the studio shows instead of the id. */\n name?: string;\n\n /** One line on what this flow does (UI lists, plan output). */\n description?: string;\n}\n\nexport interface WorkflowBuilder extends FlowBuilder {\n toIR(): WorkflowIR;\n}\n\n/**\n * Assign node ids from the structural path (rules in ir.ts's header). Written\n * in place into the tree toIR deep-cloned, never into the builder's own nodes.\n */\nfunction assignIds(nodes: NodeIR[], prefix: string): void {\n nodes.forEach((node, i) => {\n const id = prefix === '' ? String(i) : `${prefix}.${i}`;\n node.id = id;\n switch (node.type) {\n case 'branch':\n node.cases.forEach((c, ci) => assignIds(c.flow, `${id}.c${ci}`));\n if (node.otherwise) assignIds(node.otherwise, `${id}.o`);\n break;\n case 'cohort':\n node.arms.forEach((arm) => assignIds(arm.flow, `${id}.${arm.name}`));\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assignIds(node.onTimeout, `${id}.t`);\n break;\n default:\n break;\n }\n });\n}\n\n/**\n * Ids are derived, never authored, so a duplicate is always a compiler bug —\n * and a silent one: assignIds writes in place, so the second write simply wins\n * and two nodes ship as one. What that costs is concrete, because an id is the\n * durable step name (`step.do('{id}:send')`), the message idempotency key\n * (`{instanceId}:{id}`) and the cohort bucket seed: the twins share a\n * checkpoint, and the second send is suppressed as a duplicate of the first.\n * Cheap to check once at the end of the compile, so it is checked.\n */\nfunction assertUniqueIds(nodes: readonly NodeIR[], seen = new Set<string>()): void {\n for (const node of nodes) {\n if (seen.has(node.id)) {\n throw new Error(\n `toIR(): node id '${node.id}' is used twice — ids are durable step names and message idempotency keys, so they must be unique`\n );\n }\n seen.add(node.id);\n switch (node.type) {\n case 'branch':\n for (const branchCase of node.cases) assertUniqueIds(branchCase.flow, seen);\n if (node.otherwise) assertUniqueIds(node.otherwise, seen);\n break;\n case 'cohort':\n for (const arm of node.arms) assertUniqueIds(arm.flow, seen);\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) assertUniqueIds(node.onTimeout, seen);\n break;\n default:\n break;\n }\n }\n}\n\nclass WorkflowBuilderImpl extends FlowBuilderImpl implements WorkflowBuilder {\n constructor(\n private readonly options: WorkflowDefinition,\n /** Callsite of the workflow() call (provenance; lands in meta.loc). */\n private readonly loc: SourceLocIR | undefined\n ) {\n super();\n }\n\n private goalIR(): GoalIR | undefined {\n const goal = this.options.goal;\n if (goal === undefined) return undefined;\n if ('__condition' in goal) {\n return { condition: condIR(goal), exitOnMatch: true };\n }\n return {\n condition: condIR(goal.condition),\n ...(goal.within !== undefined ? { within: durationIR(goal.within) } : {}),\n exitOnMatch: goal.exitOnMatch ?? true,\n };\n }\n\n private metaIR(): MetaIR | undefined {\n const { name, description } = this.options;\n if (name === undefined && description === undefined && this.loc === undefined) {\n return undefined;\n }\n return {\n ...(name !== undefined ? { name } : {}),\n ...(description !== undefined ? { description } : {}),\n ...(this.loc !== undefined ? { loc: this.loc } : {}),\n };\n }\n\n toIR(): WorkflowIR {\n const flowNodes = structuredClone(this.nodes);\n assignIds(flowNodes, '');\n assertUniqueIds(flowNodes);\n const goal = this.goalIR();\n const meta = this.metaIR();\n const body = {\n irVersion: IR_VERSION,\n id: this.options.id,\n ...(meta !== undefined ? { meta } : {}),\n trigger: (this.options.trigger as TriggerInternal).ir,\n ...(goal !== undefined ? { goal } : {}),\n ...(this.options.exitWhen !== undefined ? { exitWhen: condIR(this.options.exitWhen) } : {}),\n flow: flowNodes,\n };\n // semanticHash strips meta / label / reason, so metadata edits leave contentHash alone\n // Self-check: the compiler's output must pass IR's authoritative schema\n return WorkflowIRSchema.parse({ ...body, contentHash: semanticHash(body) });\n }\n}\n\n/**\n * Workflow definition: an id, configuration (trigger / goal / exitWhen),\n * optional labels, and chained steps.\n *\n * export const onboarding = workflow({\n * id: 'onboarding',\n * name: 'Onboarding',\n * trigger: trigger.event(e.sign_up),\n * }).email(email.welcome);\n */\nexport function workflow(definition: WorkflowDefinition): WorkflowBuilder {\n return new WorkflowBuilderImpl(definition, captureLoc(workflow));\n}\n"],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -2,9 +2,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_hash = require("./hash.cjs");
|
|
3
3
|
const require_dsl_action = require("./dsl/action.cjs");
|
|
4
4
|
const require_dsl_refs = require("./dsl/refs.cjs");
|
|
5
|
-
const require_templates = require("./templates.cjs");
|
|
6
5
|
const require_dsl_template = require("./dsl/template.cjs");
|
|
7
6
|
const require_dsl_condition = require("./dsl/condition.cjs");
|
|
7
|
+
const require_templates = require("./templates.cjs");
|
|
8
8
|
const require_dsl_flow = require("./dsl/flow.cjs");
|
|
9
9
|
const require_dsl_trigger = require("./dsl/trigger.cjs");
|
|
10
10
|
const require_ir = require("./ir.cjs");
|
|
@@ -17,6 +17,7 @@ const require_plan = require("./plan.cjs");
|
|
|
17
17
|
const require_runtime = require("./runtime.cjs");
|
|
18
18
|
exports.ActionIR = require_ir.ActionIR;
|
|
19
19
|
exports.BundleIR = require_ir.BundleIR;
|
|
20
|
+
exports.CHANNEL_OF = require_templates.CHANNEL_OF;
|
|
20
21
|
exports.ChannelIR = require_ir.ChannelIR;
|
|
21
22
|
exports.ConditionIR = require_ir.ConditionIR;
|
|
22
23
|
exports.ConfigCompileError = require_config.ConfigCompileError;
|
|
@@ -61,6 +62,7 @@ exports.fullHash = require_hash.fullHash;
|
|
|
61
62
|
exports.gt = require_dsl_condition.gt;
|
|
62
63
|
exports.gte = require_dsl_condition.gte;
|
|
63
64
|
exports.inArray = require_dsl_condition.inArray;
|
|
65
|
+
exports.indexTemplates = require_templates.indexTemplates;
|
|
64
66
|
exports.isConfig = require_config.isConfig;
|
|
65
67
|
exports.isContract = require_contract.isContract;
|
|
66
68
|
exports.isDiscordTemplate = require_templates.isDiscordTemplate;
|
|
@@ -68,6 +70,8 @@ exports.isEmailTemplate = require_templates.isEmailTemplate;
|
|
|
68
70
|
exports.isPushTemplate = require_templates.isPushTemplate;
|
|
69
71
|
exports.isSemanticChange = require_plan.isSemanticChange;
|
|
70
72
|
exports.isSlackTemplate = require_templates.isSlackTemplate;
|
|
73
|
+
exports.isTemplate = require_templates.isTemplate;
|
|
74
|
+
exports.lookupTemplate = require_templates.lookupTemplate;
|
|
71
75
|
exports.lt = require_dsl_condition.lt;
|
|
72
76
|
exports.lte = require_dsl_condition.lte;
|
|
73
77
|
exports.ne = require_dsl_condition.ne;
|
|
@@ -94,7 +98,6 @@ exports.slackTemplate = require_templates.slackTemplate;
|
|
|
94
98
|
exports.stripMeta = require_hash.stripMeta;
|
|
95
99
|
exports.stripProvenance = require_hash.stripProvenance;
|
|
96
100
|
exports.template = require_dsl_template.template;
|
|
97
|
-
exports.templates = require_dsl_template.templates;
|
|
98
101
|
exports.trigger = require_dsl_trigger.trigger;
|
|
99
102
|
exports.user = require_dsl_refs.user;
|
|
100
103
|
exports.workflow = require_dsl_workflow.workflow;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ActionIR, BundleIR, ChannelIR, ConditionIR, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR } from "./ir.cjs";
|
|
2
|
-
import { AnyTemplate, DiscordContent, DiscordTemplate, EmailTemplate, EmailTemplateDefinition, PropsSchema, PushContent, PushTemplate, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.cjs";
|
|
2
|
+
import { AnyTemplate, CHANNEL_OF, ChannelTemplate, DiscordContent, DiscordTemplate, EmailTemplate, EmailTemplateDefinition, PropsSchema, PushContent, PushTemplate, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateRegistry, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.cjs";
|
|
3
3
|
import { ActionContext, ActionHandler, ActionRef, action } from "./dsl/action.cjs";
|
|
4
4
|
import { Duration, EventMap, TimeOfDay, UserPropertyBase, Weekday } from "./dsl/base.cjs";
|
|
5
5
|
import { EventRef, EventRefs, MessageArgs, PayloadRef, PayloadRefNode, PayloadRefs, PropInput, PropsInput, UserPropertyRef, UserRefs, event, user } from "./dsl/refs.cjs";
|
|
6
|
-
import { Channel, EmptyProps, PropsOf, TemplateFactory, TemplateRef,
|
|
6
|
+
import { Channel, EmptyProps, PropsOf, TemplateFactory, TemplateRef, template } from "./dsl/template.cjs";
|
|
7
7
|
import { Condition, PayloadWhere, PerformedOptions, SegmentDefinition, SegmentRef, and, between, contains, eq, exists, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment } from "./dsl/condition.cjs";
|
|
8
8
|
import { BranchArm, BranchCase, EmitArgs, EmitPayload, Flow, FlowBuilder, PreferOptions, PushOptions, SubFlow, flow } from "./dsl/flow.cjs";
|
|
9
9
|
import { DateTime, EventTriggerOptions, TriggerFactory, TriggerRef, trigger } from "./dsl/trigger.cjs";
|
|
@@ -15,4 +15,4 @@ import { JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, resolveRuntim
|
|
|
15
15
|
import { ApnsConfig, ConfigCompileError, DatabaseConfig, EmailAddress, EmailConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, formatEmailAddress, isConfig } from "./config.cjs";
|
|
16
16
|
import { canonicalJSON, fullHash, semanticHash, sha256Hex, stripMeta, stripProvenance } from "./hash.cjs";
|
|
17
17
|
import { BundlePlan, ChangeStatus, NodeChange, ResourceChange, WorkflowChange, isSemanticChange, plan } from "./plan.cjs";
|
|
18
|
-
export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, ChangeStatus, type Channel, ChannelIR, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef,
|
|
18
|
+
export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertSupportedSchema, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ActionIR, BundleIR, ChannelIR, ConditionIR, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR } from "./ir.mjs";
|
|
2
|
-
import { AnyTemplate, DiscordContent, DiscordTemplate, EmailTemplate, EmailTemplateDefinition, PropsSchema, PushContent, PushTemplate, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.mjs";
|
|
2
|
+
import { AnyTemplate, CHANNEL_OF, ChannelTemplate, DiscordContent, DiscordTemplate, EmailTemplate, EmailTemplateDefinition, PropsSchema, PushContent, PushTemplate, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateRegistry, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.mjs";
|
|
3
3
|
import { ActionContext, ActionHandler, ActionRef, action } from "./dsl/action.mjs";
|
|
4
4
|
import { Duration, EventMap, TimeOfDay, UserPropertyBase, Weekday } from "./dsl/base.mjs";
|
|
5
5
|
import { EventRef, EventRefs, MessageArgs, PayloadRef, PayloadRefNode, PayloadRefs, PropInput, PropsInput, UserPropertyRef, UserRefs, event, user } from "./dsl/refs.mjs";
|
|
6
|
-
import { Channel, EmptyProps, PropsOf, TemplateFactory, TemplateRef,
|
|
6
|
+
import { Channel, EmptyProps, PropsOf, TemplateFactory, TemplateRef, template } from "./dsl/template.mjs";
|
|
7
7
|
import { Condition, PayloadWhere, PerformedOptions, SegmentDefinition, SegmentRef, and, between, contains, eq, exists, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment } from "./dsl/condition.mjs";
|
|
8
8
|
import { BranchArm, BranchCase, EmitArgs, EmitPayload, Flow, FlowBuilder, PreferOptions, PushOptions, SubFlow, flow } from "./dsl/flow.mjs";
|
|
9
9
|
import { DateTime, EventTriggerOptions, TriggerFactory, TriggerRef, trigger } from "./dsl/trigger.mjs";
|
|
@@ -15,4 +15,4 @@ import { JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, resolveRuntim
|
|
|
15
15
|
import { ApnsConfig, ConfigCompileError, DatabaseConfig, EmailAddress, EmailConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, formatEmailAddress, isConfig } from "./config.mjs";
|
|
16
16
|
import { canonicalJSON, fullHash, semanticHash, sha256Hex, stripMeta, stripProvenance } from "./hash.mjs";
|
|
17
17
|
import { BundlePlan, ChangeStatus, NodeChange, ResourceChange, WorkflowChange, isSemanticChange, plan } from "./plan.mjs";
|
|
18
|
-
export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, ChangeStatus, type Channel, ChannelIR, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef,
|
|
18
|
+
export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertSupportedSchema, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { canonicalJSON, fullHash, semanticHash, sha256Hex, stripMeta, stripProvenance } from "./hash.mjs";
|
|
2
2
|
import { action } from "./dsl/action.mjs";
|
|
3
3
|
import { event, user } from "./dsl/refs.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { template, templates } from "./dsl/template.mjs";
|
|
4
|
+
import { template } from "./dsl/template.mjs";
|
|
6
5
|
import { and, between, contains, eq, exists, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment } from "./dsl/condition.mjs";
|
|
6
|
+
import { CHANNEL_OF, TemplateValidationError, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.mjs";
|
|
7
7
|
import { flow } from "./dsl/flow.mjs";
|
|
8
8
|
import { trigger } from "./dsl/trigger.mjs";
|
|
9
9
|
import { ActionIR, BundleIR, ChannelIR, ConditionIR, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR } from "./ir.mjs";
|
|
@@ -14,4 +14,4 @@ import { ConfigCompileError, apnsEnvironment, compileConfig, defineConfig, forma
|
|
|
14
14
|
import { ContractValidationError, checkEvent, defineContract, isContract } from "./contract.mjs";
|
|
15
15
|
import { isSemanticChange, plan } from "./plan.mjs";
|
|
16
16
|
import { resolveRuntime } from "./runtime.mjs";
|
|
17
|
-
export { ActionIR, BundleIR, ChannelIR, ConditionIR, ConfigCompileError, ContractValidationError, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TemplateValidationError, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR, action, and, apnsEnvironment, assertSupportedSchema, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template,
|
|
17
|
+
export { ActionIR, BundleIR, CHANNEL_OF, ChannelIR, ConditionIR, ConfigCompileError, ContractValidationError, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TemplateValidationError, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR, action, and, apnsEnvironment, assertSupportedSchema, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
|
package/dist/push.cjs
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_templates = require("./templates.cjs");
|
|
3
3
|
//#region src/push.ts
|
|
4
|
-
/** A renderer over the registry; a TemplateValidationError means nothing may be sent. */
|
|
4
|
+
/** A renderer over the registry, by template id; a TemplateValidationError means nothing may be sent. */
|
|
5
5
|
function registryPushRenderer(pushes) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return require_templates.renderPushContent(pushes[key], key, props);
|
|
9
|
-
};
|
|
6
|
+
const index = require_templates.indexTemplates("push", pushes);
|
|
7
|
+
return async (id, props) => require_templates.renderPushContent(require_templates.lookupTemplate(index, "push", id), props);
|
|
10
8
|
}
|
|
11
9
|
//#endregion
|
|
12
10
|
exports.registryPushRenderer = registryPushRenderer;
|
package/dist/push.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push.cjs","names":["renderPushContent"],"sources":["../src/push.ts"],"sourcesContent":["import type { ScalarIR } from './ir';\nimport {
|
|
1
|
+
{"version":3,"file":"push.cjs","names":["indexTemplates","renderPushContent","lookupTemplate"],"sources":["../src/push.ts"],"sourcesContent":["import type { ScalarIR } from './ir';\nimport {\n type PushContent,\n type PushTemplate,\n indexTemplates,\n lookupTemplate,\n renderPushContent,\n} from './templates';\n\n/**\n * `@shware/workflow/push` — push-notification content. A push is data, not a\n * component: a title and a body the OS renders in its own chrome, built by a\n * `pushTemplate`'s `content(parsedProps)` — final text, never placeholder-\n * expanded, so braces in user data survive. Transport is a separate concern\n * (`@shware/workflow/apns`, `@shware/workflow/fcm`).\n */\n\nexport type RenderedPush = PushContent;\n\nexport type PushRenderer = (\n template: string,\n props: Record<string, ScalarIR | undefined>\n) => Promise<RenderedPush>;\n\n/** A renderer over the registry, by template id; a TemplateValidationError means nothing may be sent. */\nexport function registryPushRenderer(pushes: Readonly<Record<string, PushTemplate>>): PushRenderer {\n const index = indexTemplates('push', pushes);\n return async (id, props) => renderPushContent(lookupTemplate(index, 'push', id), props);\n}\n"],"mappings":";;;;AAyBA,SAAgB,qBAAqB,QAA8D;CACjG,MAAM,QAAQA,kBAAAA,eAAe,QAAQ,MAAM;CAC3C,OAAO,OAAO,IAAI,UAAUC,kBAAAA,kBAAkBC,kBAAAA,eAAe,OAAO,QAAQ,EAAE,GAAG,KAAK;AACxF"}
|
package/dist/push.d.cts
CHANGED
|
@@ -10,7 +10,7 @@ import { PushContent, PushTemplate } from "./templates.cjs";
|
|
|
10
10
|
*/
|
|
11
11
|
export type RenderedPush = PushContent;
|
|
12
12
|
export type PushRenderer = (template: string, props: Record<string, ScalarIR | undefined>) => Promise<RenderedPush>;
|
|
13
|
-
/** A renderer over the registry; a TemplateValidationError means nothing may be sent. */
|
|
14
|
-
export declare function registryPushRenderer(pushes: Record<string, PushTemplate
|
|
13
|
+
/** A renderer over the registry, by template id; a TemplateValidationError means nothing may be sent. */
|
|
14
|
+
export declare function registryPushRenderer(pushes: Readonly<Record<string, PushTemplate>>): PushRenderer;
|
|
15
15
|
//#endregion
|
|
16
16
|
//# sourceMappingURL=push.d.cts.map
|
package/dist/push.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push.d.cts","names":[],"sources":["../src/push.ts"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"push.d.cts","names":[],"sources":["../src/push.ts"],"mappings":";;;;;;;;;;YAiBY,eAAe;YAEf,gBACV,kBACA,OAAO,eAAe,0BACnB,QAAQ;;wBAGG,qBAAqB,QAAQ,SAAS,eAAe,iBAAiB"}
|
package/dist/push.d.mts
CHANGED
|
@@ -10,7 +10,7 @@ import { PushContent, PushTemplate } from "./templates.mjs";
|
|
|
10
10
|
*/
|
|
11
11
|
export type RenderedPush = PushContent;
|
|
12
12
|
export type PushRenderer = (template: string, props: Record<string, ScalarIR | undefined>) => Promise<RenderedPush>;
|
|
13
|
-
/** A renderer over the registry; a TemplateValidationError means nothing may be sent. */
|
|
14
|
-
export declare function registryPushRenderer(pushes: Record<string, PushTemplate
|
|
13
|
+
/** A renderer over the registry, by template id; a TemplateValidationError means nothing may be sent. */
|
|
14
|
+
export declare function registryPushRenderer(pushes: Readonly<Record<string, PushTemplate>>): PushRenderer;
|
|
15
15
|
//#endregion
|
|
16
16
|
//# sourceMappingURL=push.d.mts.map
|
package/dist/push.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push.d.mts","names":[],"sources":["../src/push.ts"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"push.d.mts","names":[],"sources":["../src/push.ts"],"mappings":";;;;;;;;;;YAiBY,eAAe;YAEf,gBACV,kBACA,OAAO,eAAe,0BACnB,QAAQ;;wBAGG,qBAAqB,QAAQ,SAAS,eAAe,iBAAiB"}
|
package/dist/push.mjs
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { renderPushContent } from "./templates.mjs";
|
|
1
|
+
import { indexTemplates, lookupTemplate, renderPushContent } from "./templates.mjs";
|
|
2
2
|
//#region src/push.ts
|
|
3
|
-
/** A renderer over the registry; a TemplateValidationError means nothing may be sent. */
|
|
3
|
+
/** A renderer over the registry, by template id; a TemplateValidationError means nothing may be sent. */
|
|
4
4
|
function registryPushRenderer(pushes) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return renderPushContent(pushes[key], key, props);
|
|
8
|
-
};
|
|
5
|
+
const index = indexTemplates("push", pushes);
|
|
6
|
+
return async (id, props) => renderPushContent(lookupTemplate(index, "push", id), props);
|
|
9
7
|
}
|
|
10
8
|
//#endregion
|
|
11
9
|
export { registryPushRenderer };
|
package/dist/push.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push.mjs","names":[],"sources":["../src/push.ts"],"sourcesContent":["import type { ScalarIR } from './ir';\nimport {
|
|
1
|
+
{"version":3,"file":"push.mjs","names":[],"sources":["../src/push.ts"],"sourcesContent":["import type { ScalarIR } from './ir';\nimport {\n type PushContent,\n type PushTemplate,\n indexTemplates,\n lookupTemplate,\n renderPushContent,\n} from './templates';\n\n/**\n * `@shware/workflow/push` — push-notification content. A push is data, not a\n * component: a title and a body the OS renders in its own chrome, built by a\n * `pushTemplate`'s `content(parsedProps)` — final text, never placeholder-\n * expanded, so braces in user data survive. Transport is a separate concern\n * (`@shware/workflow/apns`, `@shware/workflow/fcm`).\n */\n\nexport type RenderedPush = PushContent;\n\nexport type PushRenderer = (\n template: string,\n props: Record<string, ScalarIR | undefined>\n) => Promise<RenderedPush>;\n\n/** A renderer over the registry, by template id; a TemplateValidationError means nothing may be sent. */\nexport function registryPushRenderer(pushes: Readonly<Record<string, PushTemplate>>): PushRenderer {\n const index = indexTemplates('push', pushes);\n return async (id, props) => renderPushContent(lookupTemplate(index, 'push', id), props);\n}\n"],"mappings":";;;AAyBA,SAAgB,qBAAqB,QAA8D;CACjG,MAAM,QAAQ,eAAe,QAAQ,MAAM;CAC3C,OAAO,OAAO,IAAI,UAAU,kBAAkB,eAAe,OAAO,QAAQ,EAAE,GAAG,KAAK;AACxF"}
|
package/dist/react-email.cjs
CHANGED
|
@@ -8,7 +8,7 @@ let react_dom_server = require("react-dom/server");
|
|
|
8
8
|
* registry (`react-dom` is an optional peer dependency; import this subpath
|
|
9
9
|
* only when you use it).
|
|
10
10
|
*
|
|
11
|
-
* The registry
|
|
11
|
+
* The registry holds `emailTemplate({...})` definitions, looked up by their `id`. Props are
|
|
12
12
|
* parsed against the template's schema before the component renders — a
|
|
13
13
|
* failure throws TemplateValidationError and nothing renders — and the
|
|
14
14
|
* component renders as an element, so hooks and context work. Rendering
|
|
@@ -20,10 +20,10 @@ let react_dom_server = require("react-dom/server");
|
|
|
20
20
|
*/
|
|
21
21
|
const DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
|
|
22
22
|
function registryRenderer(emails) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const template =
|
|
26
|
-
const parsed = require_templates.parseTemplateProps(template,
|
|
23
|
+
const index = require_templates.indexTemplates("email", emails);
|
|
24
|
+
return async (id, props) => {
|
|
25
|
+
const template = require_templates.lookupTemplate(index, "email", id);
|
|
26
|
+
const parsed = require_templates.parseTemplateProps(template, props);
|
|
27
27
|
const element = (0, react.createElement)(template.component, parsed);
|
|
28
28
|
return {
|
|
29
29
|
subject: template.subject,
|