@teleporthq/teleport-plugin-next-workflows 0.43.62 → 0.43.63

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.
Files changed (75) hide show
  1. package/__tests__/data-update-item-runtime.test.ts +39 -0
  2. package/__tests__/page-revalidate-node.test.ts +276 -0
  3. package/__tests__/payment-refund.test.ts +135 -0
  4. package/dist/cjs/nodes/analytics/commerce-tracking.d.ts +46 -0
  5. package/dist/cjs/nodes/analytics/commerce-tracking.d.ts.map +1 -0
  6. package/dist/cjs/nodes/analytics/commerce-tracking.js +94 -0
  7. package/dist/cjs/nodes/analytics/commerce-tracking.js.map +1 -0
  8. package/dist/cjs/nodes/cache/page-revalidate.d.ts +3 -0
  9. package/dist/cjs/nodes/cache/page-revalidate.d.ts.map +1 -0
  10. package/dist/cjs/nodes/cache/page-revalidate.js +125 -0
  11. package/dist/cjs/nodes/cache/page-revalidate.js.map +1 -0
  12. package/dist/cjs/nodes/cart/cart-add-item.d.ts.map +1 -1
  13. package/dist/cjs/nodes/cart/cart-add-item.js +22 -1
  14. package/dist/cjs/nodes/cart/cart-add-item.js.map +1 -1
  15. package/dist/cjs/nodes/cart/cart-remove-item.d.ts.map +1 -1
  16. package/dist/cjs/nodes/cart/cart-remove-item.js +23 -1
  17. package/dist/cjs/nodes/cart/cart-remove-item.js.map +1 -1
  18. package/dist/cjs/nodes/data/data-create-item.d.ts.map +1 -1
  19. package/dist/cjs/nodes/data/data-create-item.js +42 -1
  20. package/dist/cjs/nodes/data/data-create-item.js.map +1 -1
  21. package/dist/cjs/nodes/data/data-update-item.d.ts.map +1 -1
  22. package/dist/cjs/nodes/data/data-update-item.js +38 -3
  23. package/dist/cjs/nodes/data/data-update-item.js.map +1 -1
  24. package/dist/cjs/nodes/index.d.ts.map +1 -1
  25. package/dist/cjs/nodes/index.js +3 -1
  26. package/dist/cjs/nodes/index.js.map +1 -1
  27. package/dist/cjs/nodes/payment/payment-refund.d.ts +3 -0
  28. package/dist/cjs/nodes/payment/payment-refund.d.ts.map +1 -0
  29. package/dist/cjs/nodes/payment/payment-refund.js +446 -0
  30. package/dist/cjs/nodes/payment/payment-refund.js.map +1 -0
  31. package/dist/cjs/secret-collector.d.ts.map +1 -1
  32. package/dist/cjs/secret-collector.js +3 -0
  33. package/dist/cjs/secret-collector.js.map +1 -1
  34. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  35. package/dist/esm/nodes/analytics/commerce-tracking.d.ts +46 -0
  36. package/dist/esm/nodes/analytics/commerce-tracking.d.ts.map +1 -0
  37. package/dist/esm/nodes/analytics/commerce-tracking.js +90 -0
  38. package/dist/esm/nodes/analytics/commerce-tracking.js.map +1 -0
  39. package/dist/esm/nodes/cache/page-revalidate.d.ts +3 -0
  40. package/dist/esm/nodes/cache/page-revalidate.d.ts.map +1 -0
  41. package/dist/esm/nodes/cache/page-revalidate.js +122 -0
  42. package/dist/esm/nodes/cache/page-revalidate.js.map +1 -0
  43. package/dist/esm/nodes/cart/cart-add-item.d.ts.map +1 -1
  44. package/dist/esm/nodes/cart/cart-add-item.js +22 -1
  45. package/dist/esm/nodes/cart/cart-add-item.js.map +1 -1
  46. package/dist/esm/nodes/cart/cart-remove-item.d.ts.map +1 -1
  47. package/dist/esm/nodes/cart/cart-remove-item.js +23 -1
  48. package/dist/esm/nodes/cart/cart-remove-item.js.map +1 -1
  49. package/dist/esm/nodes/data/data-create-item.d.ts.map +1 -1
  50. package/dist/esm/nodes/data/data-create-item.js +42 -1
  51. package/dist/esm/nodes/data/data-create-item.js.map +1 -1
  52. package/dist/esm/nodes/data/data-update-item.d.ts.map +1 -1
  53. package/dist/esm/nodes/data/data-update-item.js +38 -3
  54. package/dist/esm/nodes/data/data-update-item.js.map +1 -1
  55. package/dist/esm/nodes/index.d.ts.map +1 -1
  56. package/dist/esm/nodes/index.js +3 -1
  57. package/dist/esm/nodes/index.js.map +1 -1
  58. package/dist/esm/nodes/payment/payment-refund.d.ts +3 -0
  59. package/dist/esm/nodes/payment/payment-refund.d.ts.map +1 -0
  60. package/dist/esm/nodes/payment/payment-refund.js +443 -0
  61. package/dist/esm/nodes/payment/payment-refund.js.map +1 -0
  62. package/dist/esm/secret-collector.d.ts.map +1 -1
  63. package/dist/esm/secret-collector.js +3 -0
  64. package/dist/esm/secret-collector.js.map +1 -1
  65. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +4 -4
  67. package/src/nodes/analytics/commerce-tracking.ts +93 -0
  68. package/src/nodes/cache/page-revalidate.ts +140 -0
  69. package/src/nodes/cart/cart-add-item.ts +35 -1
  70. package/src/nodes/cart/cart-remove-item.ts +36 -1
  71. package/src/nodes/data/data-create-item.ts +44 -1
  72. package/src/nodes/data/data-update-item.ts +39 -3
  73. package/src/nodes/index.ts +4 -0
  74. package/src/nodes/payment/payment-refund.ts +512 -0
  75. package/src/secret-collector.ts +3 -0
@@ -99,6 +99,45 @@ describe('data-update-item runtime output + null-safety', () => {
99
99
  expect(result.affected).toBe(0)
100
100
  })
101
101
 
102
+ /*
103
+ A value that could not cross the client→server hand-off arrives as the
104
+ pruner's placeholder. Written through, it REPLACES the stored content with
105
+ `{"__truncated":true}` — which is how a rich-text field carrying an inlined
106
+ image emptied a whole custom page's block tree. The column keeps what it has
107
+ instead, exactly like an undefined mapping.
108
+ */
109
+ it('keeps the stored value when the new one arrived truncated', async () => {
110
+ const calls = installFetch({ id: 'r1', updatedCount: 1 })
111
+ await handler(
112
+ {
113
+ tableName: 'teleport_pages',
114
+ filters: [{ field: 'id', operator: '=', value: 'p1' }],
115
+ columnMappings: [
116
+ { column: 'blocks', value: { __truncated: true, type: 'string' } },
117
+ { column: 'title', value: 'Launch' },
118
+ ],
119
+ },
120
+ {}
121
+ )
122
+ expect(calls).toHaveLength(1)
123
+ const sentMappings = calls[0].body.columnMappings as Array<{ column: string }>
124
+ expect(sentMappings.map((m) => m.column)).toEqual(['title'])
125
+ })
126
+
127
+ it('keeps the stored value when the new one could not be serialized', async () => {
128
+ const calls = installFetch({ id: 'r1', updatedCount: 1 })
129
+ const result = (await handler(
130
+ {
131
+ tableName: 'teleport_pages',
132
+ filters: [{ field: 'id', operator: '=', value: 'p1' }],
133
+ columnMappings: { blocks: { __serializationError: true } },
134
+ },
135
+ {}
136
+ )) as Record<string, unknown>
137
+ expect(calls).toHaveLength(0)
138
+ expect(result.updatedCount).toBe(0)
139
+ })
140
+
102
141
  it('preserves an explicit null columnMapping (intentional clear ≠ undefined)', async () => {
103
142
  const calls = installFetch({ id: 'r1', updatedCount: 1 })
104
143
  await handler(
@@ -0,0 +1,276 @@
1
+ import { nodeRegistry } from '../src'
2
+
3
+ interface RevalidateResult {
4
+ revalidated?: string[]
5
+ warning?: string
6
+ success?: boolean
7
+ error?: unknown
8
+ }
9
+
10
+ interface FetchCall {
11
+ url: string
12
+ init: {
13
+ method: string
14
+ headers: Record<string, string>
15
+ body: string
16
+ }
17
+ }
18
+
19
+ const source = () => nodeRegistry['page-revalidate'].generateHandler()
20
+
21
+ /**
22
+ * Runs the emitted handler the way the generated app does: as runtime source in
23
+ * a scope that provides only `globalThis`, `window` and `fetch`. `process` is
24
+ * reached through the injected `globalThis`, which is exactly how the handler
25
+ * has to read it to survive the browser webpack.
26
+ */
27
+ const runHandler = async (
28
+ config: Record<string, unknown>,
29
+ options: {
30
+ secret?: string
31
+ context?: Record<string, unknown>
32
+ // tslint:disable-next-line:no-any
33
+ respond?: (call: FetchCall) => any
34
+ calls?: FetchCall[]
35
+ } = {}
36
+ ): Promise<RevalidateResult> => {
37
+ const env = options.secret === undefined ? {} : { TQ_CACHE_SECRET: options.secret }
38
+ const calls = options.calls || []
39
+
40
+ // tslint:disable-next-line:no-any
41
+ const fetchImpl = async (url: string, init: any) => {
42
+ const call: FetchCall = { url, init }
43
+ calls.push(call)
44
+ if (!options.respond) {
45
+ throw new Error('offline')
46
+ }
47
+ const payload = options.respond(call)
48
+ return {
49
+ ok: payload.ok !== false,
50
+ json: async () => payload.body,
51
+ }
52
+ }
53
+
54
+ // tslint:disable-next-line:function-constructor
55
+ const factory = new Function(
56
+ 'globalThis',
57
+ 'window',
58
+ 'fetch',
59
+ `${source()}; return page_revalidate;`
60
+ )
61
+ const fn = factory({ process: { env } }, undefined, fetchImpl)
62
+
63
+ return fn(config, options.context || { __baseUrl: 'https://site.test' })
64
+ }
65
+
66
+ const expectNotFatal = (result: RevalidateResult) => {
67
+ // Mirrors `isFatalNodeResult` in the emitted runtime: `success: false`, a
68
+ // string `error`, or `error: true` HALTS the workflow.
69
+ expect(result.success).toBeUndefined()
70
+ expect(typeof result.error).not.toBe('string')
71
+ expect(result.error).not.toBe(true)
72
+ }
73
+
74
+ describe('page-revalidate node', () => {
75
+ it('is registered and runs on the server', () => {
76
+ expect(nodeRegistry['page-revalidate']).toBeDefined()
77
+ expect(nodeRegistry['page-revalidate'].nodeType).toBe('page-revalidate')
78
+ // `res.revalidate` only exists inside a server route, and server-side puts
79
+ // the rebuild in the same segment as the write it follows.
80
+ expect(nodeRegistry['page-revalidate'].executionEnv).toBe('server')
81
+ })
82
+
83
+ /**
84
+ * Handlers ship as runtime source and are re-bundled by webpack, which
85
+ * rewrites `require(`.
86
+ */
87
+ it('never requires a module', () => {
88
+ expect(source()).not.toMatch(/(?<![\w.])require\s*\(/)
89
+ })
90
+
91
+ describe('path resolution', () => {
92
+ it('posts the configured paths to the revalidate route', async () => {
93
+ const calls: FetchCall[] = []
94
+ const result = await runHandler(
95
+ { paths: ['/pages/welcome', '/pages/about'] },
96
+ {
97
+ secret: 'sekret',
98
+ calls,
99
+ respond: () => ({ body: { ok: true, revalidated: ['/pages/welcome'], failed: [] } }),
100
+ }
101
+ )
102
+
103
+ expect(calls).toHaveLength(1)
104
+ expect(calls[0].url).toBe('https://site.test/api/tq-revalidate')
105
+ expect(calls[0].init.method).toBe('POST')
106
+ expect(calls[0].init.headers['x-tq-cache-secret']).toBe('sekret')
107
+ expect(JSON.parse(calls[0].init.body)).toEqual({
108
+ paths: ['/pages/welcome', '/pages/about'],
109
+ })
110
+ expect(result.revalidated).toEqual(['/pages/welcome'])
111
+ expect(result.warning).toBeUndefined()
112
+ })
113
+
114
+ /**
115
+ * A path built from a database column routinely arrives as the bare slug,
116
+ * and `res.revalidate` only accepts a site-root-relative path.
117
+ */
118
+ it('gives a bare slug a leading slash', async () => {
119
+ const calls: FetchCall[] = []
120
+ await runHandler(
121
+ { paths: ['pages/welcome'] },
122
+ { secret: 'sekret', calls, respond: () => ({ body: { ok: true } }) }
123
+ )
124
+
125
+ expect(JSON.parse(calls[0].init.body).paths).toEqual(['/pages/welcome'])
126
+ })
127
+
128
+ /**
129
+ * An update revalidates the slug the row HAD and the slug it now has — and
130
+ * when the slug did not change, those are the same string. Rebuilding it
131
+ * twice would double the work for no effect.
132
+ */
133
+ it('drops blanks, unresolved refs and duplicates', async () => {
134
+ const calls: FetchCall[] = []
135
+ await runHandler(
136
+ {
137
+ paths: [
138
+ '/pages/welcome',
139
+ ' ',
140
+ '/pages/welcome',
141
+ null,
142
+ { type: 'workflowContext', nodeId: 'n1', path: ['value'] },
143
+ 'pages/welcome',
144
+ ],
145
+ },
146
+ { secret: 'sekret', calls, respond: () => ({ body: { ok: true } }) }
147
+ )
148
+
149
+ expect(JSON.parse(calls[0].init.body).paths).toEqual(['/pages/welcome'])
150
+ })
151
+
152
+ /*
153
+ * A step that rebuilds several rows at once resolves ONE reference to an
154
+ * array of slugs — a bulk delete cannot know at build time how many rows the
155
+ * admin will select, so it cannot configure a path per row.
156
+ */
157
+ it('flattens an array of paths resolved from a single reference', async () => {
158
+ const calls: FetchCall[] = []
159
+ await runHandler(
160
+ { paths: [['/pages/welcome', '/pages/pricing'], '/pages/about'] },
161
+ { secret: 'sekret', calls, respond: () => ({ body: { ok: true } }) }
162
+ )
163
+
164
+ expect(JSON.parse(calls[0].init.body).paths).toEqual([
165
+ '/pages/welcome',
166
+ '/pages/pricing',
167
+ '/pages/about',
168
+ ])
169
+ })
170
+
171
+ it('accepts a single path configured as a bare string', async () => {
172
+ const calls: FetchCall[] = []
173
+ await runHandler(
174
+ { paths: '/pages/welcome' },
175
+ { secret: 'sekret', calls, respond: () => ({ body: { ok: true } }) }
176
+ )
177
+
178
+ expect(JSON.parse(calls[0].init.body).paths).toEqual(['/pages/welcome'])
179
+ })
180
+
181
+ it('forwards the internal request headers the deployment protection needs', async () => {
182
+ const calls: FetchCall[] = []
183
+ await runHandler(
184
+ { paths: ['/pages/welcome'] },
185
+ {
186
+ secret: 'sekret',
187
+ calls,
188
+ context: {
189
+ __baseUrl: 'https://site.test',
190
+ __internalHeaders: { cookie: '_vercel_jwt=abc' },
191
+ },
192
+ respond: () => ({ body: { ok: true } }),
193
+ }
194
+ )
195
+
196
+ expect(calls[0].init.headers.cookie).toBe('_vercel_jwt=abc')
197
+ })
198
+ })
199
+
200
+ /**
201
+ * THE regression this node is shaped around. The runtime treats
202
+ * `success: false` / a string `error` as FATAL, so a page that could not be
203
+ * rebuilt would halt the workflow AFTER the row was already written — no
204
+ * success toast, no navigation, and an error for an action that worked. Every
205
+ * failure mode below has to report a warning instead.
206
+ */
207
+ describe('never reports failure in a shape the runtime treats as fatal', () => {
208
+ it('with no paths configured', async () => {
209
+ const calls: FetchCall[] = []
210
+ const result = await runHandler({}, { secret: 'sekret', calls })
211
+
212
+ expect(result.revalidated).toEqual([])
213
+ expect(result.warning).toContain('No page paths')
214
+ expect(calls).toHaveLength(0)
215
+ expectNotFatal(result)
216
+ })
217
+
218
+ it('with every configured path blank', async () => {
219
+ const result = await runHandler({ paths: ['', ' '] }, { secret: 'sekret' })
220
+
221
+ expect(result.revalidated).toEqual([])
222
+ expectNotFatal(result)
223
+ })
224
+
225
+ it('with no secret configured', async () => {
226
+ const calls: FetchCall[] = []
227
+ const result = await runHandler({ paths: ['/pages/welcome'] }, { calls })
228
+
229
+ expect(result.revalidated).toEqual([])
230
+ expect(result.warning).toContain('TQ_CACHE_SECRET')
231
+ // Nothing is sent without the credential the route requires.
232
+ expect(calls).toHaveLength(0)
233
+ expectNotFatal(result)
234
+ })
235
+
236
+ it('when the route answers with an error status', async () => {
237
+ const result = await runHandler(
238
+ { paths: ['/pages/welcome'] },
239
+ { secret: 'sekret', respond: () => ({ ok: false, body: { error: 'Unauthorized' } }) }
240
+ )
241
+
242
+ expect(result.revalidated).toEqual([])
243
+ expect(result.warning).toBe('Unauthorized')
244
+ expectNotFatal(result)
245
+ })
246
+
247
+ it('when the route is unreachable', async () => {
248
+ const result = await runHandler({ paths: ['/pages/welcome'] }, { secret: 'sekret' })
249
+
250
+ expect(result.revalidated).toEqual([])
251
+ expect(result.warning).toBe('offline')
252
+ expectNotFatal(result)
253
+ })
254
+
255
+ /**
256
+ * `res.revalidate` throws for a path that was never statically generated —
257
+ * the normal case for a page whose row was just deleted. The paths that DID
258
+ * rebuild are still reported.
259
+ */
260
+ it('when some paths could not be rebuilt', async () => {
261
+ const result = await runHandler(
262
+ { paths: ['/pages/welcome', '/pages/gone'] },
263
+ {
264
+ secret: 'sekret',
265
+ respond: () => ({
266
+ body: { ok: true, revalidated: ['/pages/welcome'], failed: ['/pages/gone'] },
267
+ }),
268
+ }
269
+ )
270
+
271
+ expect(result.revalidated).toEqual(['/pages/welcome'])
272
+ expect(result.warning).toContain('/pages/gone')
273
+ expectNotFatal(result)
274
+ })
275
+ })
276
+ })
@@ -0,0 +1,135 @@
1
+ import { loadHandler, HandlerFn } from './_helpers/load-handler'
2
+ import { paymentRefund } from '../src/nodes/payment/payment-refund'
3
+ import { resolveHandlerEntryName } from '../src/nodes/types'
4
+ import { collectSecrets } from '../src/secret-collector'
5
+
6
+ interface RefundResult {
7
+ success: boolean
8
+ refundId: string
9
+ amount: number
10
+ currency: string
11
+ status: string
12
+ error: string
13
+ }
14
+
15
+ describe('payment-refund', () => {
16
+ const handler: HandlerFn = loadHandler('payment-refund')
17
+ const source = paymentRefund.generateHandler()
18
+
19
+ it('is a SERVER node that continues the workflow', () => {
20
+ expect(paymentRefund.executionEnv).toBe('server')
21
+ // Unlike a charge, a refund must NOT be terminal: the steps that record it,
22
+ // restock it and email the buyer all run after it.
23
+ expect(paymentRefund.isTerminal).toBe(false)
24
+ })
25
+
26
+ it('has its provider credentials collected into the generated env', () => {
27
+ // Without this the exported project ships with no key for the refund node
28
+ // and every refund fails at runtime with "not configured".
29
+ const secrets = collectSecrets({
30
+ workflows: {
31
+ wf1: {
32
+ nodes: [
33
+ {
34
+ id: 'node-1',
35
+ type: 'payment-refund',
36
+ config: { secretKey: 'sk_test_1', clientId: 'id_1', clientSecret: 'cs_1' },
37
+ },
38
+ ],
39
+ },
40
+ },
41
+ } as never)
42
+ expect(secrets.map((secret) => secret.fieldName).sort()).toEqual([
43
+ 'clientId',
44
+ 'clientSecret',
45
+ 'secretKey',
46
+ ])
47
+ })
48
+
49
+ it('resolves its own entry point out of the concatenated source', () => {
50
+ // The entry has to be the FIRST statement-level two-parameter function, or
51
+ // the generated route calls a helper instead.
52
+ expect(resolveHandlerEntryName(source, 'payment-refund')).toBe('payment_refund')
53
+ })
54
+
55
+ it('reads process through globalThis, which webpack leaves alone', () => {
56
+ // A bare `process` is rewritten by the GUI's browser packer into a name
57
+ // that does not exist on the Vercel Node runtime.
58
+ expect(source).toContain('globalThis.process.env')
59
+ // …and nowhere is `process.env` read WITHOUT that prefix.
60
+ expect(source.replace(/globalThis\.process\.env/g, '')).not.toContain('process.env')
61
+ })
62
+
63
+ it('keeps each currency list in the SAME snapshot as the code that reads it', () => {
64
+ // A declaration in one `.toString()`'d function and its reference in
65
+ // another is the "Oo is not defined" bug — a minifier renames the two
66
+ // independently and the generated file references a name it never declares.
67
+ // Slicing the emitted source per function is the only way to check that.
68
+ for (const fnName of ['toProviderMinorUnits', 'fromProviderMinorUnits']) {
69
+ const start = source.indexOf(`function ${fnName}`)
70
+ expect(start).toBeGreaterThanOrEqual(0)
71
+ const next = source.indexOf('\nfunction ', start + 1)
72
+ const body = next === -1 ? source.slice(start) : source.slice(start, next)
73
+ expect(body).toContain('zeroDecimalCurrencies = [')
74
+ expect(body).toContain('threeDecimalCurrencies = [')
75
+ expect(body).toContain('zeroDecimalCurrencies.indexOf')
76
+ }
77
+ })
78
+
79
+ it('refuses without a payment reference instead of calling a provider', async () => {
80
+ const result = (await handler({ currency: 'usd' }, {})) as RefundResult
81
+ expect(result.success).toBe(false)
82
+ expect(result.error).toMatch(/no payment reference/i)
83
+ expect(result.refundId).toBe('')
84
+ })
85
+
86
+ it('reports missing Stripe credentials as an actionable error', async () => {
87
+ const saved = process.env.STRIPE_SECRET_KEY
88
+ delete process.env.STRIPE_SECRET_KEY
89
+ try {
90
+ const result = (await handler(
91
+ { providerType: 'stripe', paymentReference: 'pi_1', currency: 'usd' },
92
+ {}
93
+ )) as RefundResult
94
+ expect(result.success).toBe(false)
95
+ expect(result.error).toContain('STRIPE_SECRET_KEY')
96
+ // The shape stays constant on every path, so a downstream node can bind
97
+ // to it without knowing which branch ran.
98
+ expect(result).toMatchObject({ refundId: '', amount: 0, status: '' })
99
+ } finally {
100
+ if (saved !== undefined) {
101
+ process.env.STRIPE_SECRET_KEY = saved
102
+ }
103
+ }
104
+ })
105
+
106
+ it('reports missing PayPal credentials without attempting a network call', async () => {
107
+ const savedId = process.env.PAYPAL_CLIENT_ID
108
+ const savedSecret = process.env.PAYPAL_CLIENT_SECRET
109
+ delete process.env.PAYPAL_CLIENT_ID
110
+ delete process.env.PAYPAL_CLIENT_SECRET
111
+ try {
112
+ const result = (await handler(
113
+ { providerType: 'paypal', paymentReference: 'cap_1', currency: 'usd' },
114
+ {}
115
+ )) as RefundResult
116
+ expect(result.success).toBe(false)
117
+ expect(result.error).toMatch(/PayPal credentials are not configured/i)
118
+ } finally {
119
+ if (savedId !== undefined) {
120
+ process.env.PAYPAL_CLIENT_ID = savedId
121
+ }
122
+ if (savedSecret !== undefined) {
123
+ process.env.PAYPAL_CLIENT_SECRET = savedSecret
124
+ }
125
+ }
126
+ })
127
+
128
+ it('always returns an answer rather than throwing, so bookkeeping can still run', async () => {
129
+ // A declined refund is a branch the workflow takes, not a crash that takes
130
+ // the whole segment — including the steps that record what happened — down.
131
+ await expect(
132
+ handler({ providerType: 'stripe', paymentReference: 'pi_1', currency: 'usd' }, {})
133
+ ).resolves.toBeDefined()
134
+ })
135
+ })
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The one way a generated workflow handler reports a commerce funnel step.
3
+ *
4
+ * ## Why a global and not an import
5
+ *
6
+ * A node handler ships as a SERIALIZED function body (`fn.toString()`) that is
7
+ * evaluated at runtime with no module scope — it cannot import anything. The
8
+ * analytics tracker publishes `window.tpTrackCommerce` when it boots, and that
9
+ * global is the only channel a handler can reach.
10
+ *
11
+ * ## Why every call is guarded twice
12
+ *
13
+ * The global is absent on any project that did not enable analytics, and absent
14
+ * on every server-side render. These handlers run inside add-to-cart and
15
+ * checkout, where an exception costs a sale — so a missing tracker has to be a
16
+ * silent no-op, never a `ReferenceError`. The `try` covers the second failure
17
+ * mode: a tracker that is present but throws (storage disabled, a browser
18
+ * extension replacing the global) must not take the cart down with it.
19
+ *
20
+ * ## ⛔ Why a calling node must NOT import `trackCommerceStep`
21
+ *
22
+ * TypeScript's CommonJS emit rewrites a cross-module call into
23
+ * `(0, commerce_tracking_1.trackCommerceStep)(...)`. That survives `.toString()`
24
+ * verbatim, and `commerce_tracking_1` does not exist in the generated workflow
25
+ * segment — so the handler throws `ReferenceError` the first time a shopper adds
26
+ * to the cart, in production only, because nothing about it is visible in the
27
+ * TypeScript source.
28
+ *
29
+ * A caller therefore declares the name AMBIENTLY (`declare function
30
+ * trackCommerceStep`), which emits nothing and leaves a bare identifier in the
31
+ * serialized body, and appends `COMMERCE_TRACKING_HELPER_SOURCE` so the runtime
32
+ * definition travels with it. `assertHandlerHasNoModuleRefs` fails the build if
33
+ * a module reference ever creeps back in.
34
+ */
35
+ /** ES5 source of the helper, for concatenating after a serialized handler. */
36
+ export declare const COMMERCE_TRACKING_HELPER_SOURCE: string;
37
+ /**
38
+ * Fails the BUILD when a serialized handler still references a module namespace.
39
+ *
40
+ * Cheap insurance against a failure that is otherwise invisible: the TypeScript
41
+ * compiles, the tests that only read the source pass, and the handler throws in
42
+ * a shopper's browser. Called by every `generateHandler` that concatenates a
43
+ * helper.
44
+ */
45
+ export declare function assertHandlerHasNoModuleRefs(source: string, nodeType: string): string;
46
+ //# sourceMappingURL=commerce-tracking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commerce-tracking.d.ts","sourceRoot":"","sources":["../../../../src/nodes/analytics/commerce-tracking.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AA+BH,8EAA8E;AAC9E,eAAO,MAAM,+BAA+B,QAA+B,CAAA;AAQ3E;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWrF"}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ /**
3
+ * The one way a generated workflow handler reports a commerce funnel step.
4
+ *
5
+ * ## Why a global and not an import
6
+ *
7
+ * A node handler ships as a SERIALIZED function body (`fn.toString()`) that is
8
+ * evaluated at runtime with no module scope — it cannot import anything. The
9
+ * analytics tracker publishes `window.tpTrackCommerce` when it boots, and that
10
+ * global is the only channel a handler can reach.
11
+ *
12
+ * ## Why every call is guarded twice
13
+ *
14
+ * The global is absent on any project that did not enable analytics, and absent
15
+ * on every server-side render. These handlers run inside add-to-cart and
16
+ * checkout, where an exception costs a sale — so a missing tracker has to be a
17
+ * silent no-op, never a `ReferenceError`. The `try` covers the second failure
18
+ * mode: a tracker that is present but throws (storage disabled, a browser
19
+ * extension replacing the global) must not take the cart down with it.
20
+ *
21
+ * ## ⛔ Why a calling node must NOT import `trackCommerceStep`
22
+ *
23
+ * TypeScript's CommonJS emit rewrites a cross-module call into
24
+ * `(0, commerce_tracking_1.trackCommerceStep)(...)`. That survives `.toString()`
25
+ * verbatim, and `commerce_tracking_1` does not exist in the generated workflow
26
+ * segment — so the handler throws `ReferenceError` the first time a shopper adds
27
+ * to the cart, in production only, because nothing about it is visible in the
28
+ * TypeScript source.
29
+ *
30
+ * A caller therefore declares the name AMBIENTLY (`declare function
31
+ * trackCommerceStep`), which emits nothing and leaves a bare identifier in the
32
+ * serialized body, and appends `COMMERCE_TRACKING_HELPER_SOURCE` so the runtime
33
+ * definition travels with it. `assertHandlerHasNoModuleRefs` fails the build if
34
+ * a module reference ever creeps back in.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.assertHandlerHasNoModuleRefs = exports.COMMERCE_TRACKING_HELPER_SOURCE = void 0;
38
+ /**
39
+ * ⛔ EXACTLY ONE PARAMETER. Not a style choice.
40
+ *
41
+ * When this package is minified by a consumer, the handler's own entry function
42
+ * loses its conventional name, and `resolveHandlerEntryName` then identifies the
43
+ * entry by ARITY: it prefers a 2-param declaration, because every real handler
44
+ * entry is `(config, context)`. A 2-param helper concatenated after a 1-param
45
+ * entry therefore WINS, and the generated workflow calls the tracking helper
46
+ * where it meant to call add-to-cart.
47
+ *
48
+ * One object parameter keeps the helper out of that contest, and the resolver's
49
+ * positional fallback then correctly picks the entry, which is emitted first.
50
+ * `resolve-handler-entry-name.test.ts` checks every registered node type against
51
+ * exactly this, and is what caught it.
52
+ *
53
+ * Not exported either: exporting invites the cross-module call this file exists
54
+ * to prevent. It is here to be read by `.toString()` below, nothing else.
55
+ */
56
+ function trackCommerceStep(step) {
57
+ try {
58
+ const tracker = globalThis.tpTrackCommerce;
59
+ if (typeof tracker === 'function') {
60
+ ;
61
+ tracker(step.name, step.detail);
62
+ }
63
+ }
64
+ catch (e) {
65
+ /* analytics must never break a cart or a checkout */
66
+ }
67
+ }
68
+ /** ES5 source of the helper, for concatenating after a serialized handler. */
69
+ exports.COMMERCE_TRACKING_HELPER_SOURCE = trackCommerceStep.toString();
70
+ /**
71
+ * Matches the two shapes TypeScript's CJS emit produces for a cross-module
72
+ * call — `(0, ns_1.fn)(…)` and a bare `ns_1.fn(…)`.
73
+ */
74
+ const MODULE_REFERENCE = /\b[A-Za-z_$][\w$]*_\d+\s*\./;
75
+ /**
76
+ * Fails the BUILD when a serialized handler still references a module namespace.
77
+ *
78
+ * Cheap insurance against a failure that is otherwise invisible: the TypeScript
79
+ * compiles, the tests that only read the source pass, and the handler throws in
80
+ * a shopper's browser. Called by every `generateHandler` that concatenates a
81
+ * helper.
82
+ */
83
+ function assertHandlerHasNoModuleRefs(source, nodeType) {
84
+ const match = source.match(MODULE_REFERENCE);
85
+ if (match) {
86
+ throw new Error(`The "${nodeType}" handler references the module namespace "${match[0]}". ` +
87
+ 'A serialized handler has no module scope, so this throws ReferenceError at ' +
88
+ 'runtime. Declare the helper ambiently (declare function ...) and append its ' +
89
+ 'source instead of importing it.');
90
+ }
91
+ return source;
92
+ }
93
+ exports.assertHandlerHasNoModuleRefs = assertHandlerHasNoModuleRefs;
94
+ //# sourceMappingURL=commerce-tracking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commerce-tracking.js","sourceRoot":"","sources":["../../../../src/nodes/analytics/commerce-tracking.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;;;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAS,iBAAiB,CAAC,IAAwD;IACjF,IAAI;QACF,MAAM,OAAO,GAAI,UAAsC,CAAC,eAAe,CAAA;QACvE,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE;YACjC,CAAC;YAAC,OAA4D,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;SACvF;KACF;IAAC,OAAO,CAAC,EAAE;QACV,qDAAqD;KACtD;AACH,CAAC;AAED,8EAA8E;AACjE,QAAA,+BAA+B,GAAG,iBAAiB,CAAC,QAAQ,EAAE,CAAA;AAE3E;;;GAGG;AACH,MAAM,gBAAgB,GAAG,6BAA6B,CAAA;AAEtD;;;;;;;GAOG;AACH,SAAgB,4BAA4B,CAAC,MAAc,EAAE,QAAgB;IAC3E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAA;IAC5C,IAAI,KAAK,EAAE;QACT,MAAM,IAAI,KAAK,CACb,QAAQ,QAAQ,8CAA8C,KAAK,CAAC,CAAC,CAAC,KAAK;YACzE,6EAA6E;YAC7E,8EAA8E;YAC9E,iCAAiC,CACpC,CAAA;KACF;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAXD,oEAWC"}
@@ -0,0 +1,3 @@
1
+ import { NodeHandlerGenerator } from '../types';
2
+ export declare const pageRevalidate: NodeHandlerGenerator;
3
+ //# sourceMappingURL=page-revalidate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-revalidate.d.ts","sourceRoot":"","sources":["../../../../src/nodes/cache/page-revalidate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAmB,MAAM,UAAU,CAAA;AAqIhE,eAAO,MAAM,cAAc,EAAE,oBAM5B,CAAA"}