@cowliss/cli 0.8.0 → 0.9.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.
@@ -0,0 +1,12 @@
1
+ import { GuestModule, GuestOutput } from "./driver.js";
2
+ //#region src/guest/driver-emails.d.ts
3
+ /**
4
+ * The template half of the driver (spec: Guest protocol). It answers the
5
+ * two inputs a bundle built from `emails/` can get, `manifest` and
6
+ * `template`, and it is the only guest entry that imports `render.ts`:
7
+ * react and react-dom reach a bundle through this file or not at all.
8
+ * `driver.ts` is the journey half and holds everything both kinds share.
9
+ */
10
+ declare function runGuest(module: GuestModule, input: unknown): Promise<GuestOutput>;
11
+ //#endregion
12
+ export { runGuest };
@@ -1,5 +1,5 @@
1
- import { a as manifestOutputSchema, i as journeyStepOutputSchema, o as templateRenderOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-F8Yk8s1P.js";
2
- import { z } from "zod";
1
+ import { o as templateRenderOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-F8Yk8s1P.js";
2
+ import { n as readManifest, t as asTemplate } from "./driver-poSdZIj8.js";
3
3
  import { createElement } from "react";
4
4
  import { renderToStaticMarkup } from "react-dom/server.browser";
5
5
 
@@ -30,154 +30,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  enumerable: true
31
31
  }) : target, mod));
32
32
 
33
- //#endregion
34
- //#region src/guest/api.ts
35
- /**
36
- * The journaled capability implementation (spec: Guest protocol). Every
37
- * `api` method funnels into `call`, which either replays the matching
38
- * journal entry or records the pending call and hangs, letting the driver
39
- * report the suspension.
40
- */
41
- /** A replayed call did not match the journal: the execution cannot continue. */
42
- var NondeterminismError = class extends Error {
43
- expected;
44
- actual;
45
- constructor(expected, actual) {
46
- super(`Journey replay diverged: the journal recorded "${expected.name}" but the code called "${actual.name}" with different arguments`);
47
- this.name = "NondeterminismError";
48
- this.expected = expected;
49
- this.actual = actual;
50
- }
51
- };
52
- /**
53
- * Structural equality over JSON values. Key order differs between a
54
- * literal in journey code and the same object round-tripped through the
55
- * journal, so comparing serialized forms would report a divergence that
56
- * is not one.
57
- */
58
- function sameValue(a, b) {
59
- if (a === b) return true;
60
- if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => sameValue(item, b[index]));
61
- if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
62
- const left = a;
63
- const right = b;
64
- const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
65
- for (const key of keys) if (!sameValue(left[key], right[key])) return false;
66
- return true;
67
- }
68
- function formatLogArg(value) {
69
- if (typeof value === "string") return value;
70
- try {
71
- return JSON.stringify(value) ?? String(value);
72
- } catch {
73
- return String(value);
74
- }
75
- }
76
- /**
77
- * Builds the `api` a journey's `run` receives, over the step's journal.
78
- * The returned state is the driver's window into the run: the pending
79
- * call, the captured logs, and the clock the shims read.
80
- */
81
- function createApi(input, journey) {
82
- let cursor = 0;
83
- let markSuspended = () => {};
84
- const state = {
85
- clock: { now: input.now },
86
- logs: [],
87
- pending: null,
88
- suspended: new Promise((resolve) => {
89
- markSuspended = resolve;
90
- })
91
- };
92
- function call(command) {
93
- if (command.name !== "restart" && cursor >= input.limits.calls) throw new CapabilityError("call_limit_exceeded", `Journey exceeded the per-execution limit of ${input.limits.calls} capability calls; use api.restart() for loops`);
94
- const entry = input.journal[cursor];
95
- if (!entry) {
96
- if (!state.pending) {
97
- state.pending = command;
98
- markSuspended();
99
- }
100
- return new Promise(() => {});
101
- }
102
- if (entry.call.name !== command.name || !sameValue(entry.call.args, command.args)) throw new NondeterminismError(entry.call, command);
103
- cursor += 1;
104
- state.clock.now = cursor < input.journal.length ? entry.at : input.now;
105
- if (entry.result.ok) return Promise.resolve(entry.result.value);
106
- return Promise.reject(new CapabilityError(entry.result.error.code, entry.result.error.message));
107
- }
108
- return {
109
- api: {
110
- sleep: (duration) => call({
111
- name: "sleep",
112
- args: { duration }
113
- }),
114
- waitForEvent: (name, options) => call({
115
- name: "waitForEvent",
116
- args: {
117
- event: name,
118
- timeout: options.timeout
119
- }
120
- }),
121
- send: {
122
- email: (args) => call({
123
- name: "send.email",
124
- args: {
125
- template: args.template,
126
- props: args.props,
127
- from: args.from ?? journey.from,
128
- replyTo: args.replyTo
129
- }
130
- }),
131
- webhook: (args) => call({
132
- name: "send.webhook",
133
- args
134
- })
135
- },
136
- traits: {
137
- set: (key, value) => call({
138
- name: "traits.set",
139
- args: {
140
- key,
141
- value
142
- }
143
- }),
144
- unset: (key) => call({
145
- name: "traits.unset",
146
- args: { key }
147
- })
148
- },
149
- profile: { get: () => call({
150
- name: "profile.get",
151
- args: {}
152
- }) },
153
- profiles: { get: (id) => call({
154
- name: "profiles.get",
155
- args: { id }
156
- }) },
157
- events: { track: (name, properties) => call({
158
- name: "events.track",
159
- args: {
160
- event: name,
161
- properties
162
- }
163
- }) },
164
- log: (...args) => {
165
- if (cursor < input.journal.length) return;
166
- if (state.logs.length >= input.limits.logLines) return;
167
- state.logs.push({
168
- level: "info",
169
- message: args.map(formatLogArg).join(" ").slice(0, input.limits.logLineBytes)
170
- });
171
- },
172
- restart: (args) => call({
173
- name: "restart",
174
- args: { event: args?.event }
175
- })
176
- },
177
- state
178
- };
179
- }
180
-
181
33
  //#endregion
182
34
  //#region ../../node_modules/.pnpm/domelementtype@2.3.0/node_modules/domelementtype/lib/esm/index.js
183
35
  /** Types of elements found in htmlparser2's DOM */
@@ -5482,173 +5334,20 @@ function renderTemplateModule(template, props) {
5482
5334
  }
5483
5335
 
5484
5336
  //#endregion
5485
- //#region src/guest/shims.ts
5337
+ //#region src/guest/driver-emails.ts
5486
5338
  /**
5487
- * A hash of the seed string into a 32-bit state (FNV-1a). The seed derives
5488
- * from the execution id, so the same execution replays the same numbers.
5489
- */
5490
- function hashSeed(seed) {
5491
- let hash = 2166136261;
5492
- for (let index = 0; index < seed.length; index += 1) {
5493
- hash ^= seed.charCodeAt(index);
5494
- hash = Math.imul(hash, 16777619);
5495
- }
5496
- return hash >>> 0;
5497
- }
5498
- /** mulberry32: small, fast, and good enough for journey branching. */
5499
- function mulberry32(seed) {
5500
- let state = hashSeed(seed);
5501
- return () => {
5502
- state = state + 1831565813 | 0;
5503
- let value = Math.imul(state ^ state >>> 15, 1 | state);
5504
- value = value + Math.imul(value ^ value >>> 7, 61 | value) ^ value;
5505
- return ((value ^ value >>> 14) >>> 0) / 4294967296;
5506
- };
5507
- }
5508
- const BLOCKED = [
5509
- "setTimeout",
5510
- "setInterval",
5511
- "setImmediate",
5512
- "fetch",
5513
- "XMLHttpRequest",
5514
- "queueMicrotask"
5515
- ];
5516
- function blocked(name) {
5517
- return () => {
5518
- throw new CapabilityError("capability_unavailable", `${name} is not available in journeys: use the api object, which is journaled and replays exactly`);
5519
- };
5520
- }
5521
- /**
5522
- * Installs the shims and returns the undo. Call it before user code runs
5523
- * and restore in a `finally`, so a driver embedded in a Node process does
5524
- * not leak a frozen clock into its host.
5525
- */
5526
- function installShims(options) {
5527
- const globals = globalThis;
5528
- const saved = /* @__PURE__ */ new Map();
5529
- const save = (name) => {
5530
- saved.set(name, name in globals ? globals[name] : void 0);
5531
- };
5532
- const RealDate = Date;
5533
- class GuestDate extends RealDate {
5534
- constructor(...args) {
5535
- if (args.length === 0) super(options.clock.now);
5536
- else super(...args);
5537
- }
5538
- static now() {
5539
- return options.clock.now;
5540
- }
5541
- }
5542
- save("Date");
5543
- globals.Date = GuestDate;
5544
- const realRandom = Math.random;
5545
- Math.random = mulberry32(options.seed);
5546
- for (const name of BLOCKED) {
5547
- save(name);
5548
- globals[name] = blocked(name);
5549
- }
5550
- return () => {
5551
- Math.random = realRandom;
5552
- for (const [name, value] of saved) if (value === void 0) delete globals[name];
5553
- else globals[name] = value;
5554
- };
5555
- }
5556
-
5557
- //#endregion
5558
- //#region src/guest/driver.ts
5559
- function asJourney(module) {
5560
- const journey = module.default;
5561
- if (!journey || typeof journey.run !== "function") throw new Error("Journey module must default-export defineJourney({ ... }) with a run function");
5562
- return journey;
5563
- }
5564
- function asTemplate(module) {
5565
- const template = module;
5566
- if (typeof template.default !== "function" || !template.props) throw new Error("Template module must export a default component and a zod `props` schema");
5567
- return template;
5568
- }
5569
- function readManifest(module) {
5570
- if (module.default && typeof module.default.run === "function") {
5571
- const journey = asJourney(module);
5572
- return manifestOutputSchema.parse({
5573
- kind: "journey",
5574
- trigger: journey.trigger,
5575
- purpose: journey.purpose,
5576
- enrollment: journey.enrollment,
5577
- description: journey.description,
5578
- from: journey.from,
5579
- tags: journey.tags
5580
- });
5581
- }
5582
- const template = asTemplate(module);
5583
- return manifestOutputSchema.parse({
5584
- kind: "template",
5585
- tags: template.tags,
5586
- sendClass: template.sendClass ?? "marketing",
5587
- verifyLink: template.verifyLink ?? false,
5588
- unsubscribeLink: template.unsubscribeLink ?? false,
5589
- propsSchema: z.toJSONSchema(template.props, { target: "draft-2020-12" })
5590
- });
5591
- }
5592
- async function runJourneyStep(module, input) {
5593
- const journey = asJourney(module);
5594
- const { api, state } = createApi(input, journey);
5595
- const restore = installShims({
5596
- clock: state.clock,
5597
- seed: input.seed
5598
- });
5599
- try {
5600
- if (await Promise.race([journey.run(input.event, api).then(() => true), state.suspended.then(() => false)])) return journeyStepOutput({
5601
- status: "done",
5602
- logs: state.logs
5603
- });
5604
- return journeyStepOutput({
5605
- status: "suspend",
5606
- call: state.pending,
5607
- logs: state.logs
5608
- });
5609
- } catch (error) {
5610
- if (error instanceof NondeterminismError) return journeyStepOutput({
5611
- status: "error",
5612
- error: {
5613
- code: "journey_nondeterministic",
5614
- message: error.message
5615
- },
5616
- nondeterministic: {
5617
- expected: error.expected,
5618
- actual: error.actual
5619
- },
5620
- logs: state.logs
5621
- });
5622
- const thrown = error instanceof Error ? error : new Error(String(error));
5623
- return journeyStepOutput({
5624
- status: "error",
5625
- error: {
5626
- code: "journey_error",
5627
- message: thrown.message,
5628
- stack: thrown.stack
5629
- },
5630
- logs: state.logs
5631
- });
5632
- } finally {
5633
- restore();
5634
- }
5635
- }
5636
- function journeyStepOutput(output) {
5637
- return journeyStepOutputSchema.parse(output);
5638
- }
5639
- /**
5640
- * Runs one guest invocation. `module` is the tenant module namespace the
5641
- * bundle's entry imported; `input` is the raw JSON the host handed over.
5339
+ * The template half of the driver (spec: Guest protocol). It answers the
5340
+ * two inputs a bundle built from `emails/` can get, `manifest` and
5341
+ * `template`, and it is the only guest entry that imports `render.ts`:
5342
+ * react and react-dom reach a bundle through this file or not at all.
5343
+ * `driver.ts` is the journey half and holds everything both kinds share.
5642
5344
  */
5643
5345
  async function runGuest(module, input) {
5644
5346
  const parsed = guestInputSchema.parse(input);
5645
5347
  if (parsed.kind === "manifest") return readManifest(module);
5646
- if (parsed.kind === "template") return renderTemplate(module, parsed);
5647
- return runJourneyStep(module, parsed);
5648
- }
5649
- function renderTemplate(module, input) {
5650
- return templateRenderOutputSchema.parse(renderTemplateModule(asTemplate(module), input.props));
5348
+ if (parsed.kind === "journey") throw new Error("A template bundle cannot run a journey step.");
5349
+ return templateRenderOutputSchema.parse(renderTemplateModule(asTemplate(module), parsed.props));
5651
5350
  }
5652
5351
 
5653
5352
  //#endregion
5654
- export { runGuest as t };
5353
+ export { runGuest };
@@ -0,0 +1,320 @@
1
+ import { a as manifestOutputSchema, i as journeyStepOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-F8Yk8s1P.js";
2
+ import { z } from "zod";
3
+
4
+ //#region src/guest/api.ts
5
+ /**
6
+ * The journaled capability implementation (spec: Guest protocol). Every
7
+ * `api` method funnels into `call`, which either replays the matching
8
+ * journal entry or records the pending call and hangs, letting the driver
9
+ * report the suspension.
10
+ */
11
+ /** A replayed call did not match the journal: the execution cannot continue. */
12
+ var NondeterminismError = class extends Error {
13
+ expected;
14
+ actual;
15
+ constructor(expected, actual) {
16
+ super(`Journey replay diverged: the journal recorded "${expected.name}" but the code called "${actual.name}" with different arguments`);
17
+ this.name = "NondeterminismError";
18
+ this.expected = expected;
19
+ this.actual = actual;
20
+ }
21
+ };
22
+ /**
23
+ * Structural equality over JSON values. Key order differs between a
24
+ * literal in journey code and the same object round-tripped through the
25
+ * journal, so comparing serialized forms would report a divergence that
26
+ * is not one.
27
+ */
28
+ function sameValue(a, b) {
29
+ if (a === b) return true;
30
+ if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => sameValue(item, b[index]));
31
+ if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
32
+ const left = a;
33
+ const right = b;
34
+ const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
35
+ for (const key of keys) if (!sameValue(left[key], right[key])) return false;
36
+ return true;
37
+ }
38
+ function formatLogArg(value) {
39
+ if (typeof value === "string") return value;
40
+ try {
41
+ return JSON.stringify(value) ?? String(value);
42
+ } catch {
43
+ return String(value);
44
+ }
45
+ }
46
+ /**
47
+ * Builds the `api` a journey's `run` receives, over the step's journal.
48
+ * The returned state is the driver's window into the run: the pending
49
+ * call, the captured logs, and the clock the shims read.
50
+ */
51
+ function createApi(input, journey) {
52
+ let cursor = 0;
53
+ let markSuspended = () => {};
54
+ const state = {
55
+ clock: { now: input.now },
56
+ logs: [],
57
+ pending: null,
58
+ suspended: new Promise((resolve) => {
59
+ markSuspended = resolve;
60
+ })
61
+ };
62
+ function call(command) {
63
+ if (command.name !== "restart" && cursor >= input.limits.calls) throw new CapabilityError("call_limit_exceeded", `Journey exceeded the per-execution limit of ${input.limits.calls} capability calls; use api.restart() for loops`);
64
+ const entry = input.journal[cursor];
65
+ if (!entry) {
66
+ if (!state.pending) {
67
+ state.pending = command;
68
+ markSuspended();
69
+ }
70
+ return new Promise(() => {});
71
+ }
72
+ if (entry.call.name !== command.name || !sameValue(entry.call.args, command.args)) throw new NondeterminismError(entry.call, command);
73
+ cursor += 1;
74
+ state.clock.now = cursor < input.journal.length ? entry.at : input.now;
75
+ if (entry.result.ok) return Promise.resolve(entry.result.value);
76
+ return Promise.reject(new CapabilityError(entry.result.error.code, entry.result.error.message));
77
+ }
78
+ return {
79
+ api: {
80
+ sleep: (duration) => call({
81
+ name: "sleep",
82
+ args: { duration }
83
+ }),
84
+ waitForEvent: (name, options) => call({
85
+ name: "waitForEvent",
86
+ args: {
87
+ event: name,
88
+ timeout: options.timeout
89
+ }
90
+ }),
91
+ send: {
92
+ email: (args) => call({
93
+ name: "send.email",
94
+ args: {
95
+ template: args.template,
96
+ props: args.props,
97
+ from: args.from ?? journey.from,
98
+ replyTo: args.replyTo
99
+ }
100
+ }),
101
+ webhook: (args) => call({
102
+ name: "send.webhook",
103
+ args
104
+ })
105
+ },
106
+ traits: {
107
+ set: (key, value) => call({
108
+ name: "traits.set",
109
+ args: {
110
+ key,
111
+ value
112
+ }
113
+ }),
114
+ unset: (key) => call({
115
+ name: "traits.unset",
116
+ args: { key }
117
+ })
118
+ },
119
+ profile: { get: () => call({
120
+ name: "profile.get",
121
+ args: {}
122
+ }) },
123
+ profiles: { get: (id) => call({
124
+ name: "profiles.get",
125
+ args: { id }
126
+ }) },
127
+ events: { track: (name, properties) => call({
128
+ name: "events.track",
129
+ args: {
130
+ event: name,
131
+ properties
132
+ }
133
+ }) },
134
+ log: (...args) => {
135
+ if (cursor < input.journal.length) return;
136
+ if (state.logs.length >= input.limits.logLines) return;
137
+ state.logs.push({
138
+ level: "info",
139
+ message: args.map(formatLogArg).join(" ").slice(0, input.limits.logLineBytes)
140
+ });
141
+ },
142
+ restart: (args) => call({
143
+ name: "restart",
144
+ args: { event: args?.event }
145
+ })
146
+ },
147
+ state
148
+ };
149
+ }
150
+
151
+ //#endregion
152
+ //#region src/guest/shims.ts
153
+ /**
154
+ * A hash of the seed string into a 32-bit state (FNV-1a). The seed derives
155
+ * from the execution id, so the same execution replays the same numbers.
156
+ */
157
+ function hashSeed(seed) {
158
+ let hash = 2166136261;
159
+ for (let index = 0; index < seed.length; index += 1) {
160
+ hash ^= seed.charCodeAt(index);
161
+ hash = Math.imul(hash, 16777619);
162
+ }
163
+ return hash >>> 0;
164
+ }
165
+ /** mulberry32: small, fast, and good enough for journey branching. */
166
+ function mulberry32(seed) {
167
+ let state = hashSeed(seed);
168
+ return () => {
169
+ state = state + 1831565813 | 0;
170
+ let value = Math.imul(state ^ state >>> 15, 1 | state);
171
+ value = value + Math.imul(value ^ value >>> 7, 61 | value) ^ value;
172
+ return ((value ^ value >>> 14) >>> 0) / 4294967296;
173
+ };
174
+ }
175
+ const BLOCKED = [
176
+ "setTimeout",
177
+ "setInterval",
178
+ "setImmediate",
179
+ "fetch",
180
+ "XMLHttpRequest",
181
+ "queueMicrotask"
182
+ ];
183
+ function blocked(name) {
184
+ return () => {
185
+ throw new CapabilityError("capability_unavailable", `${name} is not available in journeys: use the api object, which is journaled and replays exactly`);
186
+ };
187
+ }
188
+ /**
189
+ * Installs the shims and returns the undo. Call it before user code runs
190
+ * and restore in a `finally`, so a driver embedded in a Node process does
191
+ * not leak a frozen clock into its host.
192
+ */
193
+ function installShims(options) {
194
+ const globals = globalThis;
195
+ const saved = /* @__PURE__ */ new Map();
196
+ const save = (name) => {
197
+ saved.set(name, name in globals ? globals[name] : void 0);
198
+ };
199
+ const RealDate = Date;
200
+ class GuestDate extends RealDate {
201
+ constructor(...args) {
202
+ if (args.length === 0) super(options.clock.now);
203
+ else super(...args);
204
+ }
205
+ static now() {
206
+ return options.clock.now;
207
+ }
208
+ }
209
+ save("Date");
210
+ globals.Date = GuestDate;
211
+ const realRandom = Math.random;
212
+ Math.random = mulberry32(options.seed);
213
+ for (const name of BLOCKED) {
214
+ save(name);
215
+ globals[name] = blocked(name);
216
+ }
217
+ return () => {
218
+ Math.random = realRandom;
219
+ for (const [name, value] of saved) if (value === void 0) delete globals[name];
220
+ else globals[name] = value;
221
+ };
222
+ }
223
+
224
+ //#endregion
225
+ //#region src/guest/driver.ts
226
+ function asJourney(module) {
227
+ const journey = module.default;
228
+ if (!journey || typeof journey.run !== "function") throw new Error("Journey module must default-export defineJourney({ ... }) with a run function");
229
+ return journey;
230
+ }
231
+ function asTemplate(module) {
232
+ const template = module;
233
+ if (typeof template.default !== "function" || !template.props) throw new Error("Template module must export a default component and a zod `props` schema");
234
+ return template;
235
+ }
236
+ function readManifest(module) {
237
+ if (module.default && typeof module.default.run === "function") {
238
+ const journey = asJourney(module);
239
+ return manifestOutputSchema.parse({
240
+ kind: "journey",
241
+ trigger: journey.trigger,
242
+ purpose: journey.purpose,
243
+ enrollment: journey.enrollment,
244
+ description: journey.description,
245
+ from: journey.from,
246
+ tags: journey.tags
247
+ });
248
+ }
249
+ const template = asTemplate(module);
250
+ return manifestOutputSchema.parse({
251
+ kind: "template",
252
+ tags: template.tags,
253
+ sendClass: template.sendClass ?? "marketing",
254
+ verifyLink: template.verifyLink ?? false,
255
+ unsubscribeLink: template.unsubscribeLink ?? false,
256
+ propsSchema: z.toJSONSchema(template.props, { target: "draft-2020-12" })
257
+ });
258
+ }
259
+ async function runJourneyStep(module, input) {
260
+ const journey = asJourney(module);
261
+ const { api, state } = createApi(input, journey);
262
+ const restore = installShims({
263
+ clock: state.clock,
264
+ seed: input.seed
265
+ });
266
+ try {
267
+ if (await Promise.race([journey.run(input.event, api).then(() => true), state.suspended.then(() => false)])) return journeyStepOutput({
268
+ status: "done",
269
+ logs: state.logs
270
+ });
271
+ return journeyStepOutput({
272
+ status: "suspend",
273
+ call: state.pending,
274
+ logs: state.logs
275
+ });
276
+ } catch (error) {
277
+ if (error instanceof NondeterminismError) return journeyStepOutput({
278
+ status: "error",
279
+ error: {
280
+ code: "journey_nondeterministic",
281
+ message: error.message
282
+ },
283
+ nondeterministic: {
284
+ expected: error.expected,
285
+ actual: error.actual
286
+ },
287
+ logs: state.logs
288
+ });
289
+ const thrown = error instanceof Error ? error : new Error(String(error));
290
+ return journeyStepOutput({
291
+ status: "error",
292
+ error: {
293
+ code: "journey_error",
294
+ message: thrown.message,
295
+ stack: thrown.stack
296
+ },
297
+ logs: state.logs
298
+ });
299
+ } finally {
300
+ restore();
301
+ }
302
+ }
303
+ function journeyStepOutput(output) {
304
+ return journeyStepOutputSchema.parse(output);
305
+ }
306
+ /**
307
+ * Runs one guest invocation against a journey bundle. `module` is the tenant
308
+ * module namespace the bundle's entry imported; `input` is the raw JSON the
309
+ * host handed over. A `template` input is a host bug, not a tenant one: this
310
+ * bundle was built from `journeys/` and carries no renderer.
311
+ */
312
+ async function runGuest(module, input) {
313
+ const parsed = guestInputSchema.parse(input);
314
+ if (parsed.kind === "manifest") return readManifest(module);
315
+ if (parsed.kind === "template") throw new Error("A journey bundle cannot render a template.");
316
+ return runJourneyStep(module, parsed);
317
+ }
318
+
319
+ //#endregion
320
+ export { readManifest as n, runGuest as r, asTemplate as t };
@@ -1,21 +1,34 @@
1
1
  import { a as JourneyStepOutput, o as ManifestOutput, s as TemplateRenderOutput } from "./index-EqBCZnpq.js";
2
+ import { n as Template } from "./emails-5GindgMQ.js";
2
3
  //#region src/guest/driver.d.ts
3
4
  /**
4
5
  * The in-process driver: JSON in, JSON out (spec: Guest protocol). One
5
- * bundle is one module is one journey or one template, and this turns the
6
- * three inputs into the three outputs. `wasi.ts` wraps it for the sandbox;
6
+ * bundle is one module is one journey or one template, and this turns that
7
+ * bundle's inputs into its outputs. `wasi.ts` wraps it for the sandbox;
7
8
  * the simulator and the tests call `runGuest` directly.
8
9
  *
10
+ * There are two drivers, one per kind, because a bundle only pays for the
11
+ * one it is. This is the journey half: `manifest` and `journey`. The
12
+ * template half lives in `driver-emails.ts` and is the only one that
13
+ * imports `render.ts`, so react and react-dom (197KB minified) stay out of
14
+ * every journey bundle. `build/index.ts` picks the driver by source kind
15
+ * when it composes an entry; everything both kinds need is here and
16
+ * imported by that file.
17
+ *
9
18
  * Every output is validated with the shared schema before it leaves, so a
10
19
  * guest bug surfaces here rather than as a host-side parse failure.
11
20
  */
12
21
  /** A compiled bundle's module namespace, as `import * as module` sees it. */
13
22
  type GuestModule = Record<string, unknown>;
14
23
  type GuestOutput = JourneyStepOutput | TemplateRenderOutput | ManifestOutput;
24
+ declare function asTemplate(module: GuestModule): Template;
25
+ declare function readManifest(module: GuestModule): ManifestOutput;
15
26
  /**
16
- * Runs one guest invocation. `module` is the tenant module namespace the
17
- * bundle's entry imported; `input` is the raw JSON the host handed over.
27
+ * Runs one guest invocation against a journey bundle. `module` is the tenant
28
+ * module namespace the bundle's entry imported; `input` is the raw JSON the
29
+ * host handed over. A `template` input is a host bug, not a tenant one: this
30
+ * bundle was built from `journeys/` and carries no renderer.
18
31
  */
19
32
  declare function runGuest(module: GuestModule, input: unknown): Promise<GuestOutput>;
20
33
  //#endregion
21
- export { GuestModule, GuestOutput, runGuest };
34
+ export { GuestModule, GuestOutput, asTemplate, readManifest, runGuest };
@@ -1,3 +1,3 @@
1
- import { t as runGuest } from "./driver-BXuYP007.js";
1
+ import { n as readManifest, r as runGuest, t as asTemplate } from "./driver-poSdZIj8.js";
2
2
 
3
- export { runGuest };
3
+ export { asTemplate, readManifest, runGuest };
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ import { ReactElement } from "react";
3
+ //#region ../../packages/shared/src/constants.d.ts
4
+ /**
5
+ * The two classes of send, declared on the template rather than passed per
6
+ * call so the class cannot drift between two sends of the same message.
7
+ *
8
+ * Marketing is everything a journey sends on the org's behalf: it carries
9
+ * the RFC 8058 unsubscribe headers and runs the full gate list.
10
+ * Transactional is the developer's own operational mail (a receipt, an
11
+ * export-is-ready notice): it carries neither header and skips consent and
12
+ * the per-user frequency cap, because an unsubscribe link on an invoice is
13
+ * wrong and a receipt must not silently vanish for anyone who once left a
14
+ * newsletter. Suppression, the quota, the sending pause, and the from-domain
15
+ * check still apply to both: those bound cost and protect the shared SES
16
+ * account, and none of them are about what the recipient asked for.
17
+ */
18
+ declare const SEND_CLASSES: readonly ["marketing", "transactional"];
19
+ type SendClass = (typeof SEND_CLASSES)[number];
20
+ //#endregion
21
+ //#region src/guest/emails.d.ts
22
+ /** A template's subject line, computed from the props it declares. */
23
+ type Subject<Schema extends z.ZodType> = (props: z.infer<Schema>) => string;
24
+ /**
25
+ * The shape `emails/<key>.tsx` exports. `cow build` reads `props` (turned
26
+ * into JSON Schema in the manifest and into `.cow/types.d.ts`), `subject`,
27
+ * and the optional metadata below it; the sandbox renders `default`.
28
+ */
29
+ type Template<Schema extends z.ZodType = z.ZodType> = {
30
+ default: (props: z.infer<Schema>) => ReactElement;
31
+ props: Schema;
32
+ subject: Subject<Schema> | string;
33
+ /** Defaults to "marketing". */
34
+ sendClass?: SendClass;
35
+ /** The author's labels; the dashboard's only grouping. Defaults to none. */
36
+ tags?: string[];
37
+ /** True asks the host to mint a signed `verifyUrl` prop at send time. */
38
+ verifyLink?: boolean;
39
+ /**
40
+ * True asks the host to mint a signed `unsubscribeUrl` prop at send time,
41
+ * for the author's own footer link. Leave it off and a marketing template
42
+ * still gets a platform footer with the link appended at send time.
43
+ */
44
+ unsubscribeLink?: boolean;
45
+ };
46
+ //#endregion
47
+ export { Template as n, SendClass as r, Subject as t };
@@ -1,47 +1,2 @@
1
- import { z } from "zod";
2
- import { ReactElement } from "react";
3
- //#region ../../packages/shared/src/constants.d.ts
4
- /**
5
- * The two classes of send, declared on the template rather than passed per
6
- * call so the class cannot drift between two sends of the same message.
7
- *
8
- * Marketing is everything a journey sends on the org's behalf: it carries
9
- * the RFC 8058 unsubscribe headers and runs the full gate list.
10
- * Transactional is the developer's own operational mail (a receipt, an
11
- * export-is-ready notice): it carries neither header and skips consent and
12
- * the per-user frequency cap, because an unsubscribe link on an invoice is
13
- * wrong and a receipt must not silently vanish for anyone who once left a
14
- * newsletter. Suppression, the quota, the sending pause, and the from-domain
15
- * check still apply to both: those bound cost and protect the shared SES
16
- * account, and none of them are about what the recipient asked for.
17
- */
18
- declare const SEND_CLASSES: readonly ["marketing", "transactional"];
19
- type SendClass = (typeof SEND_CLASSES)[number];
20
- //#endregion
21
- //#region src/guest/emails.d.ts
22
- /** A template's subject line, computed from the props it declares. */
23
- type Subject<Schema extends z.ZodType> = (props: z.infer<Schema>) => string;
24
- /**
25
- * The shape `emails/<key>.tsx` exports. `cow build` reads `props` (turned
26
- * into JSON Schema in the manifest and into `.cow/types.d.ts`), `subject`,
27
- * and the optional metadata below it; the sandbox renders `default`.
28
- */
29
- type Template<Schema extends z.ZodType = z.ZodType> = {
30
- default: (props: z.infer<Schema>) => ReactElement;
31
- props: Schema;
32
- subject: Subject<Schema> | string;
33
- /** Defaults to "marketing". */
34
- sendClass?: SendClass;
35
- /** The author's labels; the dashboard's only grouping. Defaults to none. */
36
- tags?: string[];
37
- /** True asks the host to mint a signed `verifyUrl` prop at send time. */
38
- verifyLink?: boolean;
39
- /**
40
- * True asks the host to mint a signed `unsubscribeUrl` prop at send time,
41
- * for the author's own footer link. Leave it off and a marketing template
42
- * still gets a platform footer with the link appended at send time.
43
- */
44
- unsubscribeLink?: boolean;
45
- };
46
- //#endregion
1
+ import { n as Template, r as SendClass, t as Subject } from "./emails-5GindgMQ.js";
47
2
  export { type SendClass, Subject, Template };
@@ -1,6 +1,8 @@
1
- import { GuestModule } from "./driver.js";
1
+ import { GuestModule, GuestOutput } from "./driver.js";
2
2
  //#region src/guest/wasi.d.ts
3
+ /** The kind-specific driver the entry hands over. */
4
+ type RunGuest = (module: GuestModule, input: unknown) => Promise<GuestOutput>;
3
5
  /** Drives one invocation of the module this bundle was built for. */
4
- declare function main(module: GuestModule): void;
6
+ declare function main(module: GuestModule, runGuest: RunGuest): void;
5
7
  //#endregion
6
8
  export { main };
@@ -1,5 +1,3 @@
1
- import { t as runGuest } from "./driver-BXuYP007.js";
2
-
3
1
  //#region src/guest/wasi.ts
4
2
  const STDIN = 0;
5
3
  const STDOUT = 1;
@@ -27,7 +25,7 @@ function write(fd, text) {
27
25
  Javy.IO.writeSync(fd, new TextEncoder().encode(text));
28
26
  }
29
27
  /** Drives one invocation of the module this bundle was built for. */
30
- function main(module) {
28
+ function main(module, runGuest) {
31
29
  const fail = (error) => {
32
30
  write(STDERR, error instanceof Error ? error.message : String(error));
33
31
  throw error;
package/dist/index.js CHANGED
@@ -8136,11 +8136,15 @@ const CONFIG_FILES = [
8136
8136
  * cannot be inlined). There is no `src/` fallback: a missing
8137
8137
  * build surfaces here as an unresolved import rather than as a second code
8138
8138
  * path whose only job is to hide it.
8139
+ *
8140
+ * `guest` and `guest-emails` are the two drivers and a bundle links exactly
8141
+ * one, picked by source kind (`DRIVER`).
8139
8142
  */
8140
8143
  const GUEST_ALIAS = {
8141
8144
  "@cowliss/cli/journeys": guestFile("journeys"),
8142
8145
  "@cowliss/cli/emails": guestFile("emails"),
8143
8146
  "@cowliss/cli/guest": guestFile("guest"),
8147
+ "@cowliss/cli/guest-emails": guestFile("guest-emails"),
8144
8148
  "@cowliss/cli/guest-wasi": guestFile("guest-wasi"),
8145
8149
  "@cowliss/cli/guest-prelude": guestFile("guest-prelude")
8146
8150
  };
@@ -8325,23 +8329,33 @@ async function bundle(projectDir, outFile, entry) {
8325
8329
  };
8326
8330
  }
8327
8331
  /**
8332
+ * The driver a bundle links, one per kind. Only the template one imports
8333
+ * the renderer, so naming it from a journey entry is what used to put react
8334
+ * and react-dom in all 95 journey bundles (`guest/driver.ts`).
8335
+ */
8336
+ const DRIVER = {
8337
+ journeys: "@cowliss/cli/guest",
8338
+ emails: "@cowliss/cli/guest-emails"
8339
+ };
8340
+ /**
8328
8341
  * The artifact the sandbox compiles: stdin in, stdout out, no exports. The
8329
8342
  * prelude is imported first on purpose: ESM evaluates in import order, and
8330
8343
  * its `MessageChannel` and `console` walls have to stand before react-dom
8331
8344
  * or tenant code initializes.
8332
8345
  */
8333
- function wasiEntry(file) {
8346
+ function wasiEntry(file, kind) {
8334
8347
  return `import "@cowliss/cli/guest-prelude";
8335
8348
  import * as module from ${JSON.stringify(file)};
8336
8349
  import { main } from "@cowliss/cli/guest-wasi";
8337
- main(module);
8350
+ import { runGuest } from ${JSON.stringify(DRIVER[kind])};
8351
+ main(module, runGuest);
8338
8352
  `;
8339
8353
  }
8340
8354
  /** The local twin: the same graph, importable by Node for the manifest and `cow test`. */
8341
- function nodeEntry(file) {
8355
+ function nodeEntry(file, kind) {
8342
8356
  return `import * as module from ${JSON.stringify(file)};
8343
8357
  export { module };
8344
- export { runGuest } from "@cowliss/cli/guest";
8358
+ export { runGuest } from ${JSON.stringify(DRIVER[kind])};
8345
8359
  `;
8346
8360
  }
8347
8361
  let messagePortsUnreffed = false;
@@ -8495,7 +8509,7 @@ async function buildProject(projectDir) {
8495
8509
  const bundles = await Promise.all(files.map(async (source) => {
8496
8510
  const nodeFile = join(outDir, "node", source.kind, `${source.key}.mjs`);
8497
8511
  const wasiFile = join(outDir, "bundles", source.kind, `${source.key}.js`);
8498
- const [wasi, node] = await Promise.all([bundle(projectDir, wasiFile, wasiEntry(source.file)), bundle(projectDir, nodeFile, nodeEntry(source.file))]);
8512
+ const [wasi, node] = await Promise.all([bundle(projectDir, wasiFile, wasiEntry(source.file, source.kind)), bundle(projectDir, nodeFile, nodeEntry(source.file, source.kind))]);
8499
8513
  await writeFile(nodeFile, node.code);
8500
8514
  await writeFile(`${nodeFile}.map`, node.map);
8501
8515
  await writeFile(wasiFile, wasi.code);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cowliss/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "cow": "./dist/index.js"
@@ -18,6 +18,10 @@
18
18
  "types": "./dist/guest/driver.d.ts",
19
19
  "default": "./dist/guest/driver.js"
20
20
  },
21
+ "./guest-emails": {
22
+ "types": "./dist/guest/driver-emails.d.ts",
23
+ "default": "./dist/guest/driver-emails.js"
24
+ },
21
25
  "./guest-wasi": {
22
26
  "types": "./dist/guest/wasi.d.ts",
23
27
  "default": "./dist/guest/wasi.js"
@@ -37,6 +41,18 @@
37
41
  "tar": "^7.5.22",
38
42
  "zod": "^4.4.3"
39
43
  },
44
+ "peerDependencies": {
45
+ "react": "^19",
46
+ "react-dom": "^19"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "react": {
50
+ "optional": true
51
+ },
52
+ "react-dom": {
53
+ "optional": true
54
+ }
55
+ },
40
56
  "devDependencies": {
41
57
  "@babel/types": "^7.29.8",
42
58
  "@cowliss/api-client": "0.0.0",