zitejs 0.9.133 → 0.9.135
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/dist/cjs/backend/index.d.ts +34 -46
- package/dist/cjs/backend/index.js +4 -0
- package/dist/cjs/backend/index.test.js +53 -42
- package/dist/cjs/sync/lib.js +15 -7
- package/dist/cjs/sync/lib.test.js +3 -3
- package/dist/cjs/vite/index.js +72 -3
- package/dist/cjs/vite/index.test.js +58 -2
- package/dist/esm/backend/index.d.ts +34 -46
- package/dist/esm/backend/index.js +4 -0
- package/dist/esm/backend/index.test.js +53 -42
- package/dist/esm/sync/lib.js +15 -7
- package/dist/esm/sync/lib.test.js +3 -3
- package/dist/esm/vite/index.js +72 -3
- package/dist/esm/vite/index.test.js +58 -2
- package/package.json +1 -1
|
@@ -7,8 +7,8 @@ export type ZiteWebhook = {
|
|
|
7
7
|
* A provider trigger: what should fire the endpoint, declared as a literal.
|
|
8
8
|
* The platform registers it with the provider at publish, verifies each
|
|
9
9
|
* delivery and queues it; the endpoint only sees the payload. A `poll`
|
|
10
|
-
* trigger runs the endpoint on a cadence
|
|
11
|
-
*
|
|
10
|
+
* trigger runs the endpoint on a cadence. Either way `execute` gets `trigger`
|
|
11
|
+
* ({@link ZiteTriggerContext}) to read the cursor and record what it handled.
|
|
12
12
|
*/
|
|
13
13
|
export type ZiteTrigger = {
|
|
14
14
|
/**
|
|
@@ -86,49 +86,47 @@ export type ZiteTriggerUnsubscribeInput = {
|
|
|
86
86
|
};
|
|
87
87
|
export type ZiteTriggerSubscribe = (input: ZiteTriggerSubscribeInput) => Promise<ZiteTriggerSubscribeResult> | ZiteTriggerSubscribeResult;
|
|
88
88
|
export type ZiteTriggerUnsubscribe = (input: ZiteTriggerUnsubscribeInput) => Promise<void> | void;
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*/
|
|
93
|
-
export type ZitePollInput = {
|
|
94
|
-
/** What the previous successful run returned as `cursor`; null on the first run. */
|
|
95
|
-
cursor: string | null;
|
|
96
|
-
/** Ids earlier runs returned as `handled`, newest last. Skip these. */
|
|
97
|
-
seen: string[];
|
|
98
|
-
};
|
|
99
|
-
/**
|
|
100
|
-
* What a provider trigger's delivery carries as `input.__webhook`, beside the
|
|
101
|
-
* provider's own body. `cursor` and `seen` work as they do for a poll: for a
|
|
102
|
-
* provider whose notification only says that something changed, `subscribe`
|
|
103
|
-
* returns a starting `cursor` and each delivery returns the next one. A
|
|
104
|
-
* trigger that keeps a cursor gets one delivery at a time.
|
|
105
|
-
*/
|
|
106
|
-
export type ZiteTriggerDelivery = {
|
|
89
|
+
/** Where a provider delivery came from; app event triggers get only this. */
|
|
90
|
+
export type ZiteTriggerDeliveryInfo = {
|
|
91
|
+
/** The delivery's headers, lowercased. */
|
|
107
92
|
headers: Record<string, string>;
|
|
108
93
|
/** The provider's id for this delivery; redeliveries share it. */
|
|
109
94
|
deliveryId?: string;
|
|
110
95
|
provider?: string;
|
|
111
|
-
/** What `subscribe` or the previous successful delivery returned as `cursor`. */
|
|
112
|
-
cursor: string | null;
|
|
113
|
-
/** Ids earlier deliveries returned as `handled`, newest last. Skip these. */
|
|
114
|
-
seen: string[];
|
|
115
96
|
};
|
|
116
|
-
/** What an app event trigger's run carries as `input.__webhook`: one event, no cursor. */
|
|
117
|
-
export type ZiteAppEventDelivery = Omit<ZiteTriggerDelivery, "cursor" | "seen">;
|
|
118
97
|
/**
|
|
119
|
-
* What a poll
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
98
|
+
* What a poll run, or a provider trigger's delivery, gets as `trigger`. Record
|
|
99
|
+
* each item acted on with `handled` as you go: it is kept even if the run
|
|
100
|
+
* later throws, so the next run skips it. The cursor staged with `advance` is
|
|
101
|
+
* committed only when the run succeeds.
|
|
123
102
|
*/
|
|
124
|
-
export type
|
|
125
|
-
cursor
|
|
126
|
-
|
|
103
|
+
export type ZiteTriggerRun = {
|
|
104
|
+
/** The last committed cursor: what `subscribe` returned, or the last successful run's `advance`. Null when there is none yet. */
|
|
105
|
+
cursor: string | null;
|
|
106
|
+
/** Whether an earlier run, or this one, recorded `id` as handled. */
|
|
107
|
+
seen(id: string): boolean;
|
|
108
|
+
/** Record an item as acted on (1 to 512 characters). */
|
|
109
|
+
handled(id: string): void;
|
|
110
|
+
/** Stage the next cursor (at most 4096 characters). The last call wins. */
|
|
111
|
+
advance(cursor: string): void;
|
|
127
112
|
};
|
|
113
|
+
export type ZiteTriggerDeliveryRun = ZiteTriggerRun & ZiteTriggerDeliveryInfo;
|
|
114
|
+
/** The `trigger` param for an endpoint declaring `TTrigger`. */
|
|
115
|
+
export type ZiteTriggerContext<TTrigger> = TTrigger extends {
|
|
116
|
+
every: string;
|
|
117
|
+
} ? ZiteTriggerRun : TTrigger extends {
|
|
118
|
+
events: string[];
|
|
119
|
+
} ? ZiteTriggerDeliveryInfo : TTrigger extends {
|
|
120
|
+
integration: string;
|
|
121
|
+
} ? ZiteTriggerDeliveryRun : never;
|
|
128
122
|
export interface ZiteRequestContext {
|
|
129
123
|
user: {
|
|
130
124
|
id: string;
|
|
131
125
|
email: string;
|
|
126
|
+
roles: Array<{
|
|
127
|
+
id: string;
|
|
128
|
+
name: string;
|
|
129
|
+
}>;
|
|
132
130
|
[key: string]: unknown;
|
|
133
131
|
};
|
|
134
132
|
userId?: string;
|
|
@@ -217,22 +215,12 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
|
|
|
217
215
|
*/
|
|
218
216
|
validatesInput?: boolean;
|
|
219
217
|
execute: (params: {
|
|
220
|
-
input:
|
|
221
|
-
every: string;
|
|
222
|
-
} ? TInput & {
|
|
223
|
-
__poll?: ZitePollInput;
|
|
224
|
-
} : TTrigger extends {
|
|
225
|
-
events: string[];
|
|
226
|
-
} ? TInput & {
|
|
227
|
-
__webhook?: ZiteAppEventDelivery;
|
|
228
|
-
} : TTrigger extends {
|
|
229
|
-
integration: string;
|
|
230
|
-
} ? TInput & {
|
|
231
|
-
__webhook?: ZiteTriggerDelivery;
|
|
232
|
-
} : TInput;
|
|
218
|
+
input: TInput;
|
|
233
219
|
context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
|
|
234
220
|
} & (TStream extends true ? {
|
|
235
221
|
stream: ZiteStreamInterface;
|
|
222
|
+
} : {}) & (TTrigger extends ZiteTrigger ? {
|
|
223
|
+
trigger?: ZiteTriggerContext<TTrigger>;
|
|
236
224
|
} : {})) => Promise<TOutput> | TOutput;
|
|
237
225
|
}
|
|
238
226
|
/**
|
|
@@ -74,6 +74,10 @@ function createEndpoint(config) {
|
|
|
74
74
|
// request callers doesn't describe them.
|
|
75
75
|
if (isPlatformTriggeredInput(params.input))
|
|
76
76
|
return execute(params);
|
|
77
|
+
// A trigger's run carries the provider's body, not a request's input.
|
|
78
|
+
if (params.trigger !== undefined) {
|
|
79
|
+
return execute(params);
|
|
80
|
+
}
|
|
77
81
|
// Same, for a fire with no input object to hold the sentinel: the runner
|
|
78
82
|
// only merges `__cron` into a plain object.
|
|
79
83
|
if (firesWithoutARequest && params.input === undefined) {
|
|
@@ -4,7 +4,7 @@ const vitest_1 = require("vitest");
|
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const index_js_1 = require("./index.js");
|
|
6
6
|
const index_js_2 = require("../runtime/index.js");
|
|
7
|
-
const requestContext = { user: { id: "u1", email: "a@b.c" } };
|
|
7
|
+
const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
8
8
|
(0, vitest_1.afterEach)(() => {
|
|
9
9
|
vitest_1.vi.unstubAllGlobals();
|
|
10
10
|
});
|
|
@@ -133,28 +133,51 @@ const requestContext = { user: { id: "u1", email: "a@b.c" } };
|
|
|
133
133
|
});
|
|
134
134
|
await (0, vitest_1.expect)(endpoint.execute({ input: undefined, context: requestContext })).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
|
135
135
|
});
|
|
136
|
-
(0, vitest_1.it)("hands a poll
|
|
136
|
+
(0, vitest_1.it)("hands a poll a typed trigger and skips the input schema for it", async () => {
|
|
137
|
+
const handled = [];
|
|
137
138
|
const endpoint = (0, index_js_1.createEndpoint)({
|
|
138
|
-
inputSchema: zod_1.z.object({}),
|
|
139
|
+
inputSchema: zod_1.z.object({ required: zod_1.z.string() }),
|
|
139
140
|
trigger: { every: "5m", integration: "stripe" },
|
|
140
|
-
execute: async ({
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
execute: async ({ trigger }) => {
|
|
142
|
+
if (!trigger)
|
|
143
|
+
return { ran: 0 };
|
|
144
|
+
const fresh = ["a", "b"].filter((id) => !trigger.seen(id));
|
|
145
|
+
for (const id of fresh)
|
|
146
|
+
trigger.handled(id);
|
|
147
|
+
trigger.advance("c2");
|
|
148
|
+
return { ran: fresh.length, from: trigger.cursor };
|
|
144
149
|
},
|
|
145
150
|
});
|
|
151
|
+
const seen = new Set(["a"]);
|
|
146
152
|
await (0, vitest_1.expect)(endpoint.execute({
|
|
147
|
-
input: {
|
|
153
|
+
input: {},
|
|
148
154
|
context: { user: null },
|
|
149
|
-
|
|
155
|
+
trigger: {
|
|
156
|
+
cursor: "c1",
|
|
157
|
+
seen: (id) => seen.has(id),
|
|
158
|
+
handled: (id) => void handled.push(id),
|
|
159
|
+
advance: () => { },
|
|
160
|
+
},
|
|
161
|
+
})).resolves.toEqual({ ran: 1, from: "c1" });
|
|
162
|
+
(0, vitest_1.expect)(handled).toEqual(["b"]);
|
|
150
163
|
});
|
|
151
|
-
(0, vitest_1.it)("
|
|
164
|
+
(0, vitest_1.it)("types trigger by the declared kind", () => {
|
|
152
165
|
(0, index_js_1.createEndpoint)({
|
|
153
|
-
inputSchema: zod_1.z.object({ action: zod_1.z.string() }),
|
|
154
166
|
trigger: { integration: "github" },
|
|
155
|
-
execute: async ({
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
execute: async ({ trigger }) => trigger?.deliveryId ?? trigger?.cursor,
|
|
168
|
+
});
|
|
169
|
+
(0, index_js_1.createEndpoint)({
|
|
170
|
+
trigger: { integration: "team-slack", events: ["message.channels"] },
|
|
171
|
+
execute: async ({ trigger }) => {
|
|
172
|
+
// @ts-expect-error an app event has no cursor API
|
|
173
|
+
trigger?.advance("x");
|
|
174
|
+
return trigger?.headers;
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
(0, index_js_1.createEndpoint)({
|
|
178
|
+
execute: async (params) => {
|
|
179
|
+
// @ts-expect-error a plain endpoint gets no trigger
|
|
180
|
+
return params.trigger;
|
|
158
181
|
},
|
|
159
182
|
});
|
|
160
183
|
});
|
|
@@ -184,40 +207,28 @@ const requestContext = { user: { id: "u1", email: "a@b.c" } };
|
|
|
184
207
|
endpointId: "onPush",
|
|
185
208
|
});
|
|
186
209
|
});
|
|
187
|
-
(0, vitest_1.it)("hands a provider
|
|
210
|
+
(0, vitest_1.it)("hands a provider delivery its body as input and the cursor API on trigger", async () => {
|
|
188
211
|
const endpoint = (0, index_js_1.createEndpoint)({
|
|
189
|
-
inputSchema: zod_1.z.object({}),
|
|
212
|
+
inputSchema: zod_1.z.object({ required: zod_1.z.string() }),
|
|
190
213
|
trigger: { integration: "googleCalendar" },
|
|
191
|
-
execute: async ({ input }) => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
},
|
|
214
|
+
execute: async ({ input, trigger }) => ({
|
|
215
|
+
body: input,
|
|
216
|
+
cursor: trigger?.cursor,
|
|
217
|
+
delivery: trigger?.deliveryId,
|
|
218
|
+
}),
|
|
196
219
|
});
|
|
197
220
|
await (0, vitest_1.expect)(endpoint.execute({
|
|
198
|
-
input: {
|
|
199
|
-
__webhook: { headers: {}, cursor: "sync1", seen: ["e1"] },
|
|
200
|
-
},
|
|
221
|
+
input: { kind: "sync" },
|
|
201
222
|
context: { user: null },
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// @ts-expect-error an app event delivery has no cursor
|
|
210
|
-
void input.__webhook?.cursor;
|
|
211
|
-
return { id: input.__webhook?.deliveryId ?? null };
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
await (0, vitest_1.expect)(endpoint.execute({
|
|
215
|
-
input: {
|
|
216
|
-
event: { text: "hi" },
|
|
217
|
-
__webhook: { headers: {}, deliveryId: "slack:Ev1" },
|
|
223
|
+
trigger: {
|
|
224
|
+
cursor: "sync1",
|
|
225
|
+
seen: () => false,
|
|
226
|
+
handled: () => { },
|
|
227
|
+
advance: () => { },
|
|
228
|
+
headers: {},
|
|
229
|
+
deliveryId: "chan:2",
|
|
218
230
|
},
|
|
219
|
-
|
|
220
|
-
})).resolves.toEqual({ id: "slack:Ev1" });
|
|
231
|
+
})).resolves.toEqual({ body: { kind: "sync" }, cursor: "sync1", delivery: "chan:2" });
|
|
221
232
|
});
|
|
222
233
|
(0, vitest_1.it)("refuses events on a trigger that also polls", () => {
|
|
223
234
|
(0, index_js_1.createEndpoint)({
|
package/dist/cjs/sync/lib.js
CHANGED
|
@@ -1232,13 +1232,13 @@ function generateBackendWrapperTs(envVarNames = []) {
|
|
|
1232
1232
|
"// Auto-generated type-narrowing wrapper. Do not edit manually.",
|
|
1233
1233
|
"// Re-exports createEndpoint with context.user typed to the app User.",
|
|
1234
1234
|
"",
|
|
1235
|
-
"import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteTrigger,
|
|
1235
|
+
"import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteTrigger, ZiteTriggerContext, ZiteWebhook } from 'zitejs/backend/base';",
|
|
1236
1236
|
"import type { User } from 'zitejs/auth';",
|
|
1237
1237
|
"",
|
|
1238
1238
|
"export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStreamInterface, ZiteTrigger, ZiteWebhook };",
|
|
1239
1239
|
// A trigger's registration exports are typed against these, and they
|
|
1240
1240
|
// only reach app code through this file.
|
|
1241
|
-
"export type {
|
|
1241
|
+
"export type { ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun, ZiteTriggerSubscribe, ZiteTriggerSubscribeInput, ZiteTriggerSubscribeResult, ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput } from 'zitejs/backend/base';",
|
|
1242
1242
|
// The pre-monorepo SDK put this in scope for every endpoint, so migrated
|
|
1243
1243
|
// code can name it. `createEndpoint` infers the same thing without it.
|
|
1244
1244
|
"export type InferSchemaType<T> = T extends { _output: infer U } ? U : T;",
|
|
@@ -1302,7 +1302,8 @@ function generateBackendWrapperTs(envVarNames = []) {
|
|
|
1302
1302
|
" User,",
|
|
1303
1303
|
" 'name' | 'emailVerified' | 'image' | 'createdAt' | 'updatedAt'",
|
|
1304
1304
|
"> &",
|
|
1305
|
-
" Pick<User, 'id' | 'email'
|
|
1305
|
+
" Pick<User, 'id' | 'email'> &",
|
|
1306
|
+
" Pick<_ZiteRequestContext['user'], 'roles'>;",
|
|
1306
1307
|
"",
|
|
1307
1308
|
'export interface ZiteRequestContext extends Omit<_ZiteRequestContext, "user"> {',
|
|
1308
1309
|
" user: ZiteEndpointUser;",
|
|
@@ -1329,11 +1330,10 @@ function generateBackendWrapperTs(envVarNames = []) {
|
|
|
1329
1330
|
" trigger?: TTrigger;",
|
|
1330
1331
|
" execute: (",
|
|
1331
1332
|
" params: {",
|
|
1332
|
-
|
|
1333
|
-
// trigger's delivery `__webhook`.
|
|
1334
|
-
" input: TTrigger extends { every: string } ? TInput & { __poll?: ZitePollInput } : TTrigger extends { events: string[] } ? TInput & { __webhook?: ZiteAppEventDelivery } : TTrigger extends { integration: string } ? TInput & { __webhook?: ZiteTriggerDelivery } : TInput;",
|
|
1333
|
+
" input: TInput;",
|
|
1335
1334
|
" context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
|
|
1336
|
-
|
|
1335
|
+
// Mirrors the base module: a trigger's run gets `trigger`.
|
|
1336
|
+
" } & (TStream extends true ? { stream: ZiteStreamInterface } : {}) & (TTrigger extends ZiteTrigger ? { trigger?: ZiteTriggerContext<TTrigger> } : {}),",
|
|
1337
1337
|
" ) => Promise<TOutput> | TOutput;",
|
|
1338
1338
|
"}",
|
|
1339
1339
|
"",
|
|
@@ -1343,6 +1343,14 @@ function generateBackendWrapperTs(envVarNames = []) {
|
|
|
1343
1343
|
" return config;",
|
|
1344
1344
|
"}",
|
|
1345
1345
|
"",
|
|
1346
|
+
"/**",
|
|
1347
|
+
" * Checks if a user has a specific role. Returns true if the user has the role, false",
|
|
1348
|
+
" * otherwise. If the user is not logged in, returns false.",
|
|
1349
|
+
" */",
|
|
1350
|
+
"export const hasRole = (context: ZiteRequestContext, roleId: string) => {",
|
|
1351
|
+
" return context.user?.roles?.some((r) => r.id === roleId);",
|
|
1352
|
+
"};",
|
|
1353
|
+
"",
|
|
1346
1354
|
// Declared in the wrapper (not a standalone .d.ts) so the merge is
|
|
1347
1355
|
// guaranteed to be in the program — every endpoint imports this file,
|
|
1348
1356
|
// while tsconfig `include` does not reliably pick up dot-directories.
|
|
@@ -390,9 +390,9 @@ const duplicateIdentifierErrorsIn = (source) => {
|
|
|
390
390
|
(0, vitest_1.it)('passes the trigger registration types and trigger inputs through to app code', () => {
|
|
391
391
|
(0, vitest_1.expect)(output).toContain('ZiteTriggerSubscribe, ZiteTriggerSubscribeInput');
|
|
392
392
|
(0, vitest_1.expect)(output).toContain('ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput');
|
|
393
|
-
(0, vitest_1.expect)(output).toContain(
|
|
394
|
-
(0, vitest_1.expect)(output).toContain('
|
|
395
|
-
(0, vitest_1.expect)(output).toContain(
|
|
393
|
+
(0, vitest_1.expect)(output).toContain('ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun');
|
|
394
|
+
(0, vitest_1.expect)(output).toContain('(TTrigger extends ZiteTrigger ? { trigger?: ZiteTriggerContext<TTrigger> } : {})');
|
|
395
|
+
(0, vitest_1.expect)(output).not.toContain('__poll');
|
|
396
396
|
(0, vitest_1.expect)(output).not.toContain('src/triggers/');
|
|
397
397
|
});
|
|
398
398
|
(0, vitest_1.it)('exports ZiteError', () => {
|
package/dist/cjs/vite/index.js
CHANGED
|
@@ -11,6 +11,41 @@ const estree_walker_1 = require("estree-walker");
|
|
|
11
11
|
const domTagNames_js_1 = require("./domTagNames.js");
|
|
12
12
|
const validExtensions = new Set([".jsx", ".tsx"]);
|
|
13
13
|
const fragmentNames = new Set(["Fragment", "React.Fragment"]);
|
|
14
|
+
// react-three-fiber reads a dashed prop as a path into the three.js object, so
|
|
15
|
+
// any data-zite-* prop that reaches one throws. Components from the three
|
|
16
|
+
// ecosystem (drei, react-spring's `animated`) spread their props onto three
|
|
17
|
+
// objects, and anything rendered in a scene may be a three object too. So a
|
|
18
|
+
// component is only tagged where it is known to render DOM: under a DOM
|
|
19
|
+
// element or drei's <Html>. In files that import the three ecosystem, a JSX
|
|
20
|
+
// tree's root is not assumed to be DOM, since scene components live there.
|
|
21
|
+
const threeIntrinsicDomNames = new Set(["audio", "line", "path", "source"]);
|
|
22
|
+
function isThreeModule(source) {
|
|
23
|
+
return (source.startsWith("@react-three/") || source === "@react-spring/three");
|
|
24
|
+
}
|
|
25
|
+
/** Local binding name -> imported name ("*" for a namespace import). */
|
|
26
|
+
function getThreeImports(ast) {
|
|
27
|
+
const imports = new Map();
|
|
28
|
+
for (const statement of ast.program.body) {
|
|
29
|
+
if (statement.type === "ImportDeclaration" &&
|
|
30
|
+
isThreeModule(statement.source.value)) {
|
|
31
|
+
for (const specifier of statement.specifiers) {
|
|
32
|
+
imports.set(specifier.local.name, specifier.type === "ImportSpecifier"
|
|
33
|
+
? specifier.imported.name ?? specifier.imported.value
|
|
34
|
+
: "*");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return imports;
|
|
39
|
+
}
|
|
40
|
+
/** The three-ecosystem name an element refers to, if it comes from one. */
|
|
41
|
+
function getThreeName(componentName, threeImports) {
|
|
42
|
+
const segments = componentName.split(".");
|
|
43
|
+
const imported = threeImports.get(segments[0]);
|
|
44
|
+
if (imported === undefined) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return imported === "*" ? segments[segments.length - 1] : imported;
|
|
48
|
+
}
|
|
14
49
|
function getComponentName(elementName) {
|
|
15
50
|
if (elementName.type === "JSXIdentifier") {
|
|
16
51
|
return elementName.name;
|
|
@@ -51,18 +86,52 @@ function ziteId() {
|
|
|
51
86
|
plugins: ["jsx", "typescript"],
|
|
52
87
|
});
|
|
53
88
|
const magicString = new magic_string_1.default(code);
|
|
89
|
+
const threeImports = getThreeImports(ast);
|
|
90
|
+
const rootContext = threeImports.size > 0 ? "unknown" : "dom";
|
|
91
|
+
// The render context each open JSXElement gives its children.
|
|
92
|
+
const contextStack = [];
|
|
54
93
|
(0, estree_walker_1.walk)(ast, {
|
|
94
|
+
leave(_node) {
|
|
95
|
+
if (_node.type === "JSXElement") {
|
|
96
|
+
contextStack.pop();
|
|
97
|
+
}
|
|
98
|
+
},
|
|
55
99
|
enter(_node) {
|
|
56
100
|
const node = _node;
|
|
57
101
|
if (node.type === "JSXElement") {
|
|
58
102
|
const openingElement = node.openingElement;
|
|
59
103
|
const children = node.children;
|
|
104
|
+
const context = contextStack[contextStack.length - 1] ?? rootContext;
|
|
60
105
|
const componentName = getComponentName(openingElement.name);
|
|
61
|
-
|
|
106
|
+
const kind = componentName ? classifyElement(componentName) : null;
|
|
107
|
+
const threeName = componentName
|
|
108
|
+
? getThreeName(componentName, threeImports)
|
|
109
|
+
: null;
|
|
110
|
+
let childContext = context;
|
|
111
|
+
if (threeName === "Html") {
|
|
112
|
+
childContext = "dom";
|
|
113
|
+
}
|
|
114
|
+
else if (threeName !== null) {
|
|
115
|
+
childContext = "scene";
|
|
116
|
+
}
|
|
117
|
+
else if (kind === "dom") {
|
|
118
|
+
childContext = "dom";
|
|
119
|
+
}
|
|
120
|
+
else if (componentName &&
|
|
121
|
+
kind === null &&
|
|
122
|
+
!fragmentNames.has(componentName)) {
|
|
123
|
+
childContext = "scene";
|
|
124
|
+
}
|
|
125
|
+
contextStack.push(childContext);
|
|
126
|
+
if (!componentName || !kind) {
|
|
62
127
|
return;
|
|
63
128
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
129
|
+
const isTagged = kind === "component"
|
|
130
|
+
? threeName === "Canvas" ||
|
|
131
|
+
(threeName === null && context === "dom")
|
|
132
|
+
: !threeIntrinsicDomNames.has(componentName) ||
|
|
133
|
+
context === "dom";
|
|
134
|
+
if (!isTagged) {
|
|
66
135
|
return;
|
|
67
136
|
}
|
|
68
137
|
const line = openingElement.loc?.start?.line ?? 0;
|
|
@@ -42,9 +42,65 @@ async function transform(code) {
|
|
|
42
42
|
const source = 'const a = <group><mesh position-x={1} /><ambientLight intensity={0.5} /></group>;';
|
|
43
43
|
(0, vitest_1.expect)(await transform(source)).toBe(source);
|
|
44
44
|
});
|
|
45
|
-
(0, vitest_1.it)('tags DOM
|
|
45
|
+
(0, vitest_1.it)('tags DOM but not components nested inside non-DOM intrinsics', async () => {
|
|
46
46
|
const code = await transform('const a = <group><Html><div /></Html></group>;');
|
|
47
|
-
(0, vitest_1.expect)(code).toBe('const a = <group><Html
|
|
47
|
+
(0, vitest_1.expect)(code).toBe('const a = <group><Html><div data-zite-id="src/App.tsx|1|23" /></Html></group>;');
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.describe)('react-three scenes', () => {
|
|
50
|
+
const scene = (jsx) => transform("import { Canvas } from '@react-three/fiber';\n" +
|
|
51
|
+
"import { Html, Text } from '@react-three/drei';\n" +
|
|
52
|
+
"import { Button } from './Button';\n" +
|
|
53
|
+
`const a = ${jsx};`);
|
|
54
|
+
(0, vitest_1.it)('skips three components, nested components and three.js intrinsics', async () => {
|
|
55
|
+
const code = await scene('<Canvas><Text>Hi</Text><Button /><line /></Canvas>');
|
|
56
|
+
(0, vitest_1.expect)(code).toContain('<Text>Hi</Text>');
|
|
57
|
+
(0, vitest_1.expect)(code).toContain('<Button />');
|
|
58
|
+
(0, vitest_1.expect)(code).toContain('<line />');
|
|
59
|
+
});
|
|
60
|
+
(0, vitest_1.it)('tags the Canvas container and UI next to it', async () => {
|
|
61
|
+
const code = await scene('<div><Canvas /><Button>Start</Button></div>');
|
|
62
|
+
(0, vitest_1.expect)(code).toMatch(/<Canvas data-zite-id="[^"]+" data-zite-component="Canvas" \/>/);
|
|
63
|
+
(0, vitest_1.expect)(code).toMatch(/<Button data-zite-id="[^"]+" data-zite-component="Button" data-zite-editable="true">/);
|
|
64
|
+
});
|
|
65
|
+
(0, vitest_1.it)('tags DOM and components inside an <Html> overlay again', async () => {
|
|
66
|
+
const code = await scene('<Canvas><Html><Button /><div /></Html></Canvas>');
|
|
67
|
+
(0, vitest_1.expect)(code).toContain('<Html>');
|
|
68
|
+
(0, vitest_1.expect)(code).toMatch(/<Button data-zite-id="[^"]+" data-zite-component="Button" \/>/);
|
|
69
|
+
(0, vitest_1.expect)(code).toMatch(/<div data-zite-id="[^"]+" \/>/);
|
|
70
|
+
});
|
|
71
|
+
(0, vitest_1.it)('skips the root of a scene component in the same file', async () => {
|
|
72
|
+
const code = await scene('<><Button position={[0, 0, 0]} /></>');
|
|
73
|
+
(0, vitest_1.expect)(code).toContain('<Button position={[0, 0, 0]} />');
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)('skips children of three wrapper components', async () => {
|
|
76
|
+
const code = await transform("import { Float } from '@react-three/drei';\nimport { Box } from './Box';\nconst a = <Float><Box /></Float>;");
|
|
77
|
+
(0, vitest_1.expect)(code).toContain('<Float><Box /></Float>');
|
|
78
|
+
});
|
|
79
|
+
(0, vitest_1.it)('recognises aliased and namespaced three imports', async () => {
|
|
80
|
+
const code = await transform("import { Canvas as C } from '@react-three/fiber';\nimport * as Drei from '@react-three/drei';\n" +
|
|
81
|
+
"import { Box, Card } from './ui';\nconst a = <C><Box /><Drei.Html><Card /></Drei.Html></C>;");
|
|
82
|
+
(0, vitest_1.expect)(code).toMatch(/<C data-zite-id="[^"]+" data-zite-component="C">/);
|
|
83
|
+
(0, vitest_1.expect)(code).toContain('<Box />');
|
|
84
|
+
(0, vitest_1.expect)(code).toContain('<Drei.Html>');
|
|
85
|
+
(0, vitest_1.expect)(code).toMatch(/<Card data-zite-id="[^"]+" data-zite-component="Card" \/>/);
|
|
86
|
+
});
|
|
87
|
+
(0, vitest_1.it)('tags SVG lines and paths in DOM next to a scene', async () => {
|
|
88
|
+
const code = await scene('<div><svg><line /><path /></svg></div>');
|
|
89
|
+
(0, vitest_1.expect)(code).toMatch(/<line data-zite-id="[^"]+" \/>/);
|
|
90
|
+
(0, vitest_1.expect)(code).toMatch(/<path data-zite-id="[^"]+" \/>/);
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.it)("skips react-spring's animated three elements", async () => {
|
|
93
|
+
const code = await transform("import { animated } from '@react-spring/three';\nconst a = <animated.mesh />;");
|
|
94
|
+
(0, vitest_1.expect)(code).toContain('<animated.mesh />');
|
|
95
|
+
});
|
|
96
|
+
(0, vitest_1.it)('skips components nested in a scene element in a file without three imports', async () => {
|
|
97
|
+
const code = await transform("import { Tree } from './Tree';\nconst a = <group><Tree /></group>;");
|
|
98
|
+
(0, vitest_1.expect)(code).toContain('<Tree />');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
(0, vitest_1.it)('keeps tagging components in files that do not import react-three', async () => {
|
|
102
|
+
const code = await transform("import { Card } from './Card';\nconst a = <Card />;");
|
|
103
|
+
(0, vitest_1.expect)(code).toContain('<Card data-zite-id="src/App.tsx|2|10" data-zite-component="Card" />');
|
|
48
104
|
});
|
|
49
105
|
(0, vitest_1.it)('leaves fragments untouched', async () => {
|
|
50
106
|
const source = 'const a = <Fragment><React.Fragment key="a" /></Fragment>;';
|
|
@@ -7,8 +7,8 @@ export type ZiteWebhook = {
|
|
|
7
7
|
* A provider trigger: what should fire the endpoint, declared as a literal.
|
|
8
8
|
* The platform registers it with the provider at publish, verifies each
|
|
9
9
|
* delivery and queues it; the endpoint only sees the payload. A `poll`
|
|
10
|
-
* trigger runs the endpoint on a cadence
|
|
11
|
-
*
|
|
10
|
+
* trigger runs the endpoint on a cadence. Either way `execute` gets `trigger`
|
|
11
|
+
* ({@link ZiteTriggerContext}) to read the cursor and record what it handled.
|
|
12
12
|
*/
|
|
13
13
|
export type ZiteTrigger = {
|
|
14
14
|
/**
|
|
@@ -86,49 +86,47 @@ export type ZiteTriggerUnsubscribeInput = {
|
|
|
86
86
|
};
|
|
87
87
|
export type ZiteTriggerSubscribe = (input: ZiteTriggerSubscribeInput) => Promise<ZiteTriggerSubscribeResult> | ZiteTriggerSubscribeResult;
|
|
88
88
|
export type ZiteTriggerUnsubscribe = (input: ZiteTriggerUnsubscribeInput) => Promise<void> | void;
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*/
|
|
93
|
-
export type ZitePollInput = {
|
|
94
|
-
/** What the previous successful run returned as `cursor`; null on the first run. */
|
|
95
|
-
cursor: string | null;
|
|
96
|
-
/** Ids earlier runs returned as `handled`, newest last. Skip these. */
|
|
97
|
-
seen: string[];
|
|
98
|
-
};
|
|
99
|
-
/**
|
|
100
|
-
* What a provider trigger's delivery carries as `input.__webhook`, beside the
|
|
101
|
-
* provider's own body. `cursor` and `seen` work as they do for a poll: for a
|
|
102
|
-
* provider whose notification only says that something changed, `subscribe`
|
|
103
|
-
* returns a starting `cursor` and each delivery returns the next one. A
|
|
104
|
-
* trigger that keeps a cursor gets one delivery at a time.
|
|
105
|
-
*/
|
|
106
|
-
export type ZiteTriggerDelivery = {
|
|
89
|
+
/** Where a provider delivery came from; app event triggers get only this. */
|
|
90
|
+
export type ZiteTriggerDeliveryInfo = {
|
|
91
|
+
/** The delivery's headers, lowercased. */
|
|
107
92
|
headers: Record<string, string>;
|
|
108
93
|
/** The provider's id for this delivery; redeliveries share it. */
|
|
109
94
|
deliveryId?: string;
|
|
110
95
|
provider?: string;
|
|
111
|
-
/** What `subscribe` or the previous successful delivery returned as `cursor`. */
|
|
112
|
-
cursor: string | null;
|
|
113
|
-
/** Ids earlier deliveries returned as `handled`, newest last. Skip these. */
|
|
114
|
-
seen: string[];
|
|
115
96
|
};
|
|
116
|
-
/** What an app event trigger's run carries as `input.__webhook`: one event, no cursor. */
|
|
117
|
-
export type ZiteAppEventDelivery = Omit<ZiteTriggerDelivery, "cursor" | "seen">;
|
|
118
97
|
/**
|
|
119
|
-
* What a poll
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
98
|
+
* What a poll run, or a provider trigger's delivery, gets as `trigger`. Record
|
|
99
|
+
* each item acted on with `handled` as you go: it is kept even if the run
|
|
100
|
+
* later throws, so the next run skips it. The cursor staged with `advance` is
|
|
101
|
+
* committed only when the run succeeds.
|
|
123
102
|
*/
|
|
124
|
-
export type
|
|
125
|
-
cursor
|
|
126
|
-
|
|
103
|
+
export type ZiteTriggerRun = {
|
|
104
|
+
/** The last committed cursor: what `subscribe` returned, or the last successful run's `advance`. Null when there is none yet. */
|
|
105
|
+
cursor: string | null;
|
|
106
|
+
/** Whether an earlier run, or this one, recorded `id` as handled. */
|
|
107
|
+
seen(id: string): boolean;
|
|
108
|
+
/** Record an item as acted on (1 to 512 characters). */
|
|
109
|
+
handled(id: string): void;
|
|
110
|
+
/** Stage the next cursor (at most 4096 characters). The last call wins. */
|
|
111
|
+
advance(cursor: string): void;
|
|
127
112
|
};
|
|
113
|
+
export type ZiteTriggerDeliveryRun = ZiteTriggerRun & ZiteTriggerDeliveryInfo;
|
|
114
|
+
/** The `trigger` param for an endpoint declaring `TTrigger`. */
|
|
115
|
+
export type ZiteTriggerContext<TTrigger> = TTrigger extends {
|
|
116
|
+
every: string;
|
|
117
|
+
} ? ZiteTriggerRun : TTrigger extends {
|
|
118
|
+
events: string[];
|
|
119
|
+
} ? ZiteTriggerDeliveryInfo : TTrigger extends {
|
|
120
|
+
integration: string;
|
|
121
|
+
} ? ZiteTriggerDeliveryRun : never;
|
|
128
122
|
export interface ZiteRequestContext {
|
|
129
123
|
user: {
|
|
130
124
|
id: string;
|
|
131
125
|
email: string;
|
|
126
|
+
roles: Array<{
|
|
127
|
+
id: string;
|
|
128
|
+
name: string;
|
|
129
|
+
}>;
|
|
132
130
|
[key: string]: unknown;
|
|
133
131
|
};
|
|
134
132
|
userId?: string;
|
|
@@ -217,22 +215,12 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
|
|
|
217
215
|
*/
|
|
218
216
|
validatesInput?: boolean;
|
|
219
217
|
execute: (params: {
|
|
220
|
-
input:
|
|
221
|
-
every: string;
|
|
222
|
-
} ? TInput & {
|
|
223
|
-
__poll?: ZitePollInput;
|
|
224
|
-
} : TTrigger extends {
|
|
225
|
-
events: string[];
|
|
226
|
-
} ? TInput & {
|
|
227
|
-
__webhook?: ZiteAppEventDelivery;
|
|
228
|
-
} : TTrigger extends {
|
|
229
|
-
integration: string;
|
|
230
|
-
} ? TInput & {
|
|
231
|
-
__webhook?: ZiteTriggerDelivery;
|
|
232
|
-
} : TInput;
|
|
218
|
+
input: TInput;
|
|
233
219
|
context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
|
|
234
220
|
} & (TStream extends true ? {
|
|
235
221
|
stream: ZiteStreamInterface;
|
|
222
|
+
} : {}) & (TTrigger extends ZiteTrigger ? {
|
|
223
|
+
trigger?: ZiteTriggerContext<TTrigger>;
|
|
236
224
|
} : {})) => Promise<TOutput> | TOutput;
|
|
237
225
|
}
|
|
238
226
|
/**
|
|
@@ -69,6 +69,10 @@ export function createEndpoint(config) {
|
|
|
69
69
|
// request callers doesn't describe them.
|
|
70
70
|
if (isPlatformTriggeredInput(params.input))
|
|
71
71
|
return execute(params);
|
|
72
|
+
// A trigger's run carries the provider's body, not a request's input.
|
|
73
|
+
if (params.trigger !== undefined) {
|
|
74
|
+
return execute(params);
|
|
75
|
+
}
|
|
72
76
|
// Same, for a fire with no input object to hold the sentinel: the runner
|
|
73
77
|
// only merges `__cron` into a plain object.
|
|
74
78
|
if (firesWithoutARequest && params.input === undefined) {
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, afterEach, vi } from "vitest";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { createEndpoint, ZiteError, } from "./index.js";
|
|
4
4
|
import { createTableClient } from "../runtime/index.js";
|
|
5
|
-
const requestContext = { user: { id: "u1", email: "a@b.c" } };
|
|
5
|
+
const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
6
6
|
afterEach(() => {
|
|
7
7
|
vi.unstubAllGlobals();
|
|
8
8
|
});
|
|
@@ -131,28 +131,51 @@ describe("platform-triggered runs", () => {
|
|
|
131
131
|
});
|
|
132
132
|
await expect(endpoint.execute({ input: undefined, context: requestContext })).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
|
133
133
|
});
|
|
134
|
-
it("hands a poll
|
|
134
|
+
it("hands a poll a typed trigger and skips the input schema for it", async () => {
|
|
135
|
+
const handled = [];
|
|
135
136
|
const endpoint = createEndpoint({
|
|
136
|
-
inputSchema: z.object({}),
|
|
137
|
+
inputSchema: z.object({ required: z.string() }),
|
|
137
138
|
trigger: { every: "5m", integration: "stripe" },
|
|
138
|
-
execute: async ({
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
execute: async ({ trigger }) => {
|
|
140
|
+
if (!trigger)
|
|
141
|
+
return { ran: 0 };
|
|
142
|
+
const fresh = ["a", "b"].filter((id) => !trigger.seen(id));
|
|
143
|
+
for (const id of fresh)
|
|
144
|
+
trigger.handled(id);
|
|
145
|
+
trigger.advance("c2");
|
|
146
|
+
return { ran: fresh.length, from: trigger.cursor };
|
|
142
147
|
},
|
|
143
148
|
});
|
|
149
|
+
const seen = new Set(["a"]);
|
|
144
150
|
await expect(endpoint.execute({
|
|
145
|
-
input: {
|
|
151
|
+
input: {},
|
|
146
152
|
context: { user: null },
|
|
147
|
-
|
|
153
|
+
trigger: {
|
|
154
|
+
cursor: "c1",
|
|
155
|
+
seen: (id) => seen.has(id),
|
|
156
|
+
handled: (id) => void handled.push(id),
|
|
157
|
+
advance: () => { },
|
|
158
|
+
},
|
|
159
|
+
})).resolves.toEqual({ ran: 1, from: "c1" });
|
|
160
|
+
expect(handled).toEqual(["b"]);
|
|
148
161
|
});
|
|
149
|
-
it("
|
|
162
|
+
it("types trigger by the declared kind", () => {
|
|
150
163
|
createEndpoint({
|
|
151
|
-
inputSchema: z.object({ action: z.string() }),
|
|
152
164
|
trigger: { integration: "github" },
|
|
153
|
-
execute: async ({
|
|
154
|
-
|
|
155
|
-
|
|
165
|
+
execute: async ({ trigger }) => trigger?.deliveryId ?? trigger?.cursor,
|
|
166
|
+
});
|
|
167
|
+
createEndpoint({
|
|
168
|
+
trigger: { integration: "team-slack", events: ["message.channels"] },
|
|
169
|
+
execute: async ({ trigger }) => {
|
|
170
|
+
// @ts-expect-error an app event has no cursor API
|
|
171
|
+
trigger?.advance("x");
|
|
172
|
+
return trigger?.headers;
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
createEndpoint({
|
|
176
|
+
execute: async (params) => {
|
|
177
|
+
// @ts-expect-error a plain endpoint gets no trigger
|
|
178
|
+
return params.trigger;
|
|
156
179
|
},
|
|
157
180
|
});
|
|
158
181
|
});
|
|
@@ -182,40 +205,28 @@ describe("platform-triggered runs", () => {
|
|
|
182
205
|
endpointId: "onPush",
|
|
183
206
|
});
|
|
184
207
|
});
|
|
185
|
-
it("hands a provider
|
|
208
|
+
it("hands a provider delivery its body as input and the cursor API on trigger", async () => {
|
|
186
209
|
const endpoint = createEndpoint({
|
|
187
|
-
inputSchema: z.object({}),
|
|
210
|
+
inputSchema: z.object({ required: z.string() }),
|
|
188
211
|
trigger: { integration: "googleCalendar" },
|
|
189
|
-
execute: async ({ input }) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
},
|
|
212
|
+
execute: async ({ input, trigger }) => ({
|
|
213
|
+
body: input,
|
|
214
|
+
cursor: trigger?.cursor,
|
|
215
|
+
delivery: trigger?.deliveryId,
|
|
216
|
+
}),
|
|
194
217
|
});
|
|
195
218
|
await expect(endpoint.execute({
|
|
196
|
-
input: {
|
|
197
|
-
__webhook: { headers: {}, cursor: "sync1", seen: ["e1"] },
|
|
198
|
-
},
|
|
219
|
+
input: { kind: "sync" },
|
|
199
220
|
context: { user: null },
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// @ts-expect-error an app event delivery has no cursor
|
|
208
|
-
void input.__webhook?.cursor;
|
|
209
|
-
return { id: input.__webhook?.deliveryId ?? null };
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
await expect(endpoint.execute({
|
|
213
|
-
input: {
|
|
214
|
-
event: { text: "hi" },
|
|
215
|
-
__webhook: { headers: {}, deliveryId: "slack:Ev1" },
|
|
221
|
+
trigger: {
|
|
222
|
+
cursor: "sync1",
|
|
223
|
+
seen: () => false,
|
|
224
|
+
handled: () => { },
|
|
225
|
+
advance: () => { },
|
|
226
|
+
headers: {},
|
|
227
|
+
deliveryId: "chan:2",
|
|
216
228
|
},
|
|
217
|
-
|
|
218
|
-
})).resolves.toEqual({ id: "slack:Ev1" });
|
|
229
|
+
})).resolves.toEqual({ body: { kind: "sync" }, cursor: "sync1", delivery: "chan:2" });
|
|
219
230
|
});
|
|
220
231
|
it("refuses events on a trigger that also polls", () => {
|
|
221
232
|
createEndpoint({
|
package/dist/esm/sync/lib.js
CHANGED
|
@@ -1217,13 +1217,13 @@ export function generateBackendWrapperTs(envVarNames = []) {
|
|
|
1217
1217
|
"// Auto-generated type-narrowing wrapper. Do not edit manually.",
|
|
1218
1218
|
"// Re-exports createEndpoint with context.user typed to the app User.",
|
|
1219
1219
|
"",
|
|
1220
|
-
"import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteTrigger,
|
|
1220
|
+
"import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteTrigger, ZiteTriggerContext, ZiteWebhook } from 'zitejs/backend/base';",
|
|
1221
1221
|
"import type { User } from 'zitejs/auth';",
|
|
1222
1222
|
"",
|
|
1223
1223
|
"export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStreamInterface, ZiteTrigger, ZiteWebhook };",
|
|
1224
1224
|
// A trigger's registration exports are typed against these, and they
|
|
1225
1225
|
// only reach app code through this file.
|
|
1226
|
-
"export type {
|
|
1226
|
+
"export type { ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun, ZiteTriggerSubscribe, ZiteTriggerSubscribeInput, ZiteTriggerSubscribeResult, ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput } from 'zitejs/backend/base';",
|
|
1227
1227
|
// The pre-monorepo SDK put this in scope for every endpoint, so migrated
|
|
1228
1228
|
// code can name it. `createEndpoint` infers the same thing without it.
|
|
1229
1229
|
"export type InferSchemaType<T> = T extends { _output: infer U } ? U : T;",
|
|
@@ -1287,7 +1287,8 @@ export function generateBackendWrapperTs(envVarNames = []) {
|
|
|
1287
1287
|
" User,",
|
|
1288
1288
|
" 'name' | 'emailVerified' | 'image' | 'createdAt' | 'updatedAt'",
|
|
1289
1289
|
"> &",
|
|
1290
|
-
" Pick<User, 'id' | 'email'
|
|
1290
|
+
" Pick<User, 'id' | 'email'> &",
|
|
1291
|
+
" Pick<_ZiteRequestContext['user'], 'roles'>;",
|
|
1291
1292
|
"",
|
|
1292
1293
|
'export interface ZiteRequestContext extends Omit<_ZiteRequestContext, "user"> {',
|
|
1293
1294
|
" user: ZiteEndpointUser;",
|
|
@@ -1314,11 +1315,10 @@ export function generateBackendWrapperTs(envVarNames = []) {
|
|
|
1314
1315
|
" trigger?: TTrigger;",
|
|
1315
1316
|
" execute: (",
|
|
1316
1317
|
" params: {",
|
|
1317
|
-
|
|
1318
|
-
// trigger's delivery `__webhook`.
|
|
1319
|
-
" input: TTrigger extends { every: string } ? TInput & { __poll?: ZitePollInput } : TTrigger extends { events: string[] } ? TInput & { __webhook?: ZiteAppEventDelivery } : TTrigger extends { integration: string } ? TInput & { __webhook?: ZiteTriggerDelivery } : TInput;",
|
|
1318
|
+
" input: TInput;",
|
|
1320
1319
|
" context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
|
|
1321
|
-
|
|
1320
|
+
// Mirrors the base module: a trigger's run gets `trigger`.
|
|
1321
|
+
" } & (TStream extends true ? { stream: ZiteStreamInterface } : {}) & (TTrigger extends ZiteTrigger ? { trigger?: ZiteTriggerContext<TTrigger> } : {}),",
|
|
1322
1322
|
" ) => Promise<TOutput> | TOutput;",
|
|
1323
1323
|
"}",
|
|
1324
1324
|
"",
|
|
@@ -1328,6 +1328,14 @@ export function generateBackendWrapperTs(envVarNames = []) {
|
|
|
1328
1328
|
" return config;",
|
|
1329
1329
|
"}",
|
|
1330
1330
|
"",
|
|
1331
|
+
"/**",
|
|
1332
|
+
" * Checks if a user has a specific role. Returns true if the user has the role, false",
|
|
1333
|
+
" * otherwise. If the user is not logged in, returns false.",
|
|
1334
|
+
" */",
|
|
1335
|
+
"export const hasRole = (context: ZiteRequestContext, roleId: string) => {",
|
|
1336
|
+
" return context.user?.roles?.some((r) => r.id === roleId);",
|
|
1337
|
+
"};",
|
|
1338
|
+
"",
|
|
1331
1339
|
// Declared in the wrapper (not a standalone .d.ts) so the merge is
|
|
1332
1340
|
// guaranteed to be in the program — every endpoint imports this file,
|
|
1333
1341
|
// while tsconfig `include` does not reliably pick up dot-directories.
|
|
@@ -385,9 +385,9 @@ describe('generateBackendWrapperTs', () => {
|
|
|
385
385
|
it('passes the trigger registration types and trigger inputs through to app code', () => {
|
|
386
386
|
expect(output).toContain('ZiteTriggerSubscribe, ZiteTriggerSubscribeInput');
|
|
387
387
|
expect(output).toContain('ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput');
|
|
388
|
-
expect(output).toContain(
|
|
389
|
-
expect(output).toContain('
|
|
390
|
-
expect(output).toContain(
|
|
388
|
+
expect(output).toContain('ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun');
|
|
389
|
+
expect(output).toContain('(TTrigger extends ZiteTrigger ? { trigger?: ZiteTriggerContext<TTrigger> } : {})');
|
|
390
|
+
expect(output).not.toContain('__poll');
|
|
391
391
|
expect(output).not.toContain('src/triggers/');
|
|
392
392
|
});
|
|
393
393
|
it('exports ZiteError', () => {
|
package/dist/esm/vite/index.js
CHANGED
|
@@ -5,6 +5,41 @@ import { walk } from "estree-walker";
|
|
|
5
5
|
import { domTagNames } from "./domTagNames.js";
|
|
6
6
|
const validExtensions = new Set([".jsx", ".tsx"]);
|
|
7
7
|
const fragmentNames = new Set(["Fragment", "React.Fragment"]);
|
|
8
|
+
// react-three-fiber reads a dashed prop as a path into the three.js object, so
|
|
9
|
+
// any data-zite-* prop that reaches one throws. Components from the three
|
|
10
|
+
// ecosystem (drei, react-spring's `animated`) spread their props onto three
|
|
11
|
+
// objects, and anything rendered in a scene may be a three object too. So a
|
|
12
|
+
// component is only tagged where it is known to render DOM: under a DOM
|
|
13
|
+
// element or drei's <Html>. In files that import the three ecosystem, a JSX
|
|
14
|
+
// tree's root is not assumed to be DOM, since scene components live there.
|
|
15
|
+
const threeIntrinsicDomNames = new Set(["audio", "line", "path", "source"]);
|
|
16
|
+
function isThreeModule(source) {
|
|
17
|
+
return (source.startsWith("@react-three/") || source === "@react-spring/three");
|
|
18
|
+
}
|
|
19
|
+
/** Local binding name -> imported name ("*" for a namespace import). */
|
|
20
|
+
function getThreeImports(ast) {
|
|
21
|
+
const imports = new Map();
|
|
22
|
+
for (const statement of ast.program.body) {
|
|
23
|
+
if (statement.type === "ImportDeclaration" &&
|
|
24
|
+
isThreeModule(statement.source.value)) {
|
|
25
|
+
for (const specifier of statement.specifiers) {
|
|
26
|
+
imports.set(specifier.local.name, specifier.type === "ImportSpecifier"
|
|
27
|
+
? specifier.imported.name ?? specifier.imported.value
|
|
28
|
+
: "*");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return imports;
|
|
33
|
+
}
|
|
34
|
+
/** The three-ecosystem name an element refers to, if it comes from one. */
|
|
35
|
+
function getThreeName(componentName, threeImports) {
|
|
36
|
+
const segments = componentName.split(".");
|
|
37
|
+
const imported = threeImports.get(segments[0]);
|
|
38
|
+
if (imported === undefined) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return imported === "*" ? segments[segments.length - 1] : imported;
|
|
42
|
+
}
|
|
8
43
|
function getComponentName(elementName) {
|
|
9
44
|
if (elementName.type === "JSXIdentifier") {
|
|
10
45
|
return elementName.name;
|
|
@@ -45,18 +80,52 @@ export function ziteId() {
|
|
|
45
80
|
plugins: ["jsx", "typescript"],
|
|
46
81
|
});
|
|
47
82
|
const magicString = new MagicString(code);
|
|
83
|
+
const threeImports = getThreeImports(ast);
|
|
84
|
+
const rootContext = threeImports.size > 0 ? "unknown" : "dom";
|
|
85
|
+
// The render context each open JSXElement gives its children.
|
|
86
|
+
const contextStack = [];
|
|
48
87
|
walk(ast, {
|
|
88
|
+
leave(_node) {
|
|
89
|
+
if (_node.type === "JSXElement") {
|
|
90
|
+
contextStack.pop();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
49
93
|
enter(_node) {
|
|
50
94
|
const node = _node;
|
|
51
95
|
if (node.type === "JSXElement") {
|
|
52
96
|
const openingElement = node.openingElement;
|
|
53
97
|
const children = node.children;
|
|
98
|
+
const context = contextStack[contextStack.length - 1] ?? rootContext;
|
|
54
99
|
const componentName = getComponentName(openingElement.name);
|
|
55
|
-
|
|
100
|
+
const kind = componentName ? classifyElement(componentName) : null;
|
|
101
|
+
const threeName = componentName
|
|
102
|
+
? getThreeName(componentName, threeImports)
|
|
103
|
+
: null;
|
|
104
|
+
let childContext = context;
|
|
105
|
+
if (threeName === "Html") {
|
|
106
|
+
childContext = "dom";
|
|
107
|
+
}
|
|
108
|
+
else if (threeName !== null) {
|
|
109
|
+
childContext = "scene";
|
|
110
|
+
}
|
|
111
|
+
else if (kind === "dom") {
|
|
112
|
+
childContext = "dom";
|
|
113
|
+
}
|
|
114
|
+
else if (componentName &&
|
|
115
|
+
kind === null &&
|
|
116
|
+
!fragmentNames.has(componentName)) {
|
|
117
|
+
childContext = "scene";
|
|
118
|
+
}
|
|
119
|
+
contextStack.push(childContext);
|
|
120
|
+
if (!componentName || !kind) {
|
|
56
121
|
return;
|
|
57
122
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
123
|
+
const isTagged = kind === "component"
|
|
124
|
+
? threeName === "Canvas" ||
|
|
125
|
+
(threeName === null && context === "dom")
|
|
126
|
+
: !threeIntrinsicDomNames.has(componentName) ||
|
|
127
|
+
context === "dom";
|
|
128
|
+
if (!isTagged) {
|
|
60
129
|
return;
|
|
61
130
|
}
|
|
62
131
|
const line = openingElement.loc?.start?.line ?? 0;
|
|
@@ -37,9 +37,65 @@ describe('ziteId', () => {
|
|
|
37
37
|
const source = 'const a = <group><mesh position-x={1} /><ambientLight intensity={0.5} /></group>;';
|
|
38
38
|
expect(await transform(source)).toBe(source);
|
|
39
39
|
});
|
|
40
|
-
it('tags DOM
|
|
40
|
+
it('tags DOM but not components nested inside non-DOM intrinsics', async () => {
|
|
41
41
|
const code = await transform('const a = <group><Html><div /></Html></group>;');
|
|
42
|
-
expect(code).toBe('const a = <group><Html
|
|
42
|
+
expect(code).toBe('const a = <group><Html><div data-zite-id="src/App.tsx|1|23" /></Html></group>;');
|
|
43
|
+
});
|
|
44
|
+
describe('react-three scenes', () => {
|
|
45
|
+
const scene = (jsx) => transform("import { Canvas } from '@react-three/fiber';\n" +
|
|
46
|
+
"import { Html, Text } from '@react-three/drei';\n" +
|
|
47
|
+
"import { Button } from './Button';\n" +
|
|
48
|
+
`const a = ${jsx};`);
|
|
49
|
+
it('skips three components, nested components and three.js intrinsics', async () => {
|
|
50
|
+
const code = await scene('<Canvas><Text>Hi</Text><Button /><line /></Canvas>');
|
|
51
|
+
expect(code).toContain('<Text>Hi</Text>');
|
|
52
|
+
expect(code).toContain('<Button />');
|
|
53
|
+
expect(code).toContain('<line />');
|
|
54
|
+
});
|
|
55
|
+
it('tags the Canvas container and UI next to it', async () => {
|
|
56
|
+
const code = await scene('<div><Canvas /><Button>Start</Button></div>');
|
|
57
|
+
expect(code).toMatch(/<Canvas data-zite-id="[^"]+" data-zite-component="Canvas" \/>/);
|
|
58
|
+
expect(code).toMatch(/<Button data-zite-id="[^"]+" data-zite-component="Button" data-zite-editable="true">/);
|
|
59
|
+
});
|
|
60
|
+
it('tags DOM and components inside an <Html> overlay again', async () => {
|
|
61
|
+
const code = await scene('<Canvas><Html><Button /><div /></Html></Canvas>');
|
|
62
|
+
expect(code).toContain('<Html>');
|
|
63
|
+
expect(code).toMatch(/<Button data-zite-id="[^"]+" data-zite-component="Button" \/>/);
|
|
64
|
+
expect(code).toMatch(/<div data-zite-id="[^"]+" \/>/);
|
|
65
|
+
});
|
|
66
|
+
it('skips the root of a scene component in the same file', async () => {
|
|
67
|
+
const code = await scene('<><Button position={[0, 0, 0]} /></>');
|
|
68
|
+
expect(code).toContain('<Button position={[0, 0, 0]} />');
|
|
69
|
+
});
|
|
70
|
+
it('skips children of three wrapper components', async () => {
|
|
71
|
+
const code = await transform("import { Float } from '@react-three/drei';\nimport { Box } from './Box';\nconst a = <Float><Box /></Float>;");
|
|
72
|
+
expect(code).toContain('<Float><Box /></Float>');
|
|
73
|
+
});
|
|
74
|
+
it('recognises aliased and namespaced three imports', async () => {
|
|
75
|
+
const code = await transform("import { Canvas as C } from '@react-three/fiber';\nimport * as Drei from '@react-three/drei';\n" +
|
|
76
|
+
"import { Box, Card } from './ui';\nconst a = <C><Box /><Drei.Html><Card /></Drei.Html></C>;");
|
|
77
|
+
expect(code).toMatch(/<C data-zite-id="[^"]+" data-zite-component="C">/);
|
|
78
|
+
expect(code).toContain('<Box />');
|
|
79
|
+
expect(code).toContain('<Drei.Html>');
|
|
80
|
+
expect(code).toMatch(/<Card data-zite-id="[^"]+" data-zite-component="Card" \/>/);
|
|
81
|
+
});
|
|
82
|
+
it('tags SVG lines and paths in DOM next to a scene', async () => {
|
|
83
|
+
const code = await scene('<div><svg><line /><path /></svg></div>');
|
|
84
|
+
expect(code).toMatch(/<line data-zite-id="[^"]+" \/>/);
|
|
85
|
+
expect(code).toMatch(/<path data-zite-id="[^"]+" \/>/);
|
|
86
|
+
});
|
|
87
|
+
it("skips react-spring's animated three elements", async () => {
|
|
88
|
+
const code = await transform("import { animated } from '@react-spring/three';\nconst a = <animated.mesh />;");
|
|
89
|
+
expect(code).toContain('<animated.mesh />');
|
|
90
|
+
});
|
|
91
|
+
it('skips components nested in a scene element in a file without three imports', async () => {
|
|
92
|
+
const code = await transform("import { Tree } from './Tree';\nconst a = <group><Tree /></group>;");
|
|
93
|
+
expect(code).toContain('<Tree />');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it('keeps tagging components in files that do not import react-three', async () => {
|
|
97
|
+
const code = await transform("import { Card } from './Card';\nconst a = <Card />;");
|
|
98
|
+
expect(code).toContain('<Card data-zite-id="src/App.tsx|2|10" data-zite-component="Card" />');
|
|
43
99
|
});
|
|
44
100
|
it('leaves fragments untouched', async () => {
|
|
45
101
|
const source = 'const a = <Fragment><React.Fragment key="a" /></Fragment>;';
|