@zaaxch/tailframe 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tailframe.mjs +30 -12
- package/package.json +1 -1
- package/src/architecture.mjs +15 -10
- package/src/config.mjs +59 -0
- package/src/conventions.mjs +22 -5
- package/src/flutter.mjs +111 -0
- package/src/generate.mjs +3 -2
- package/src/new.mjs +207 -52
- package/src/owned-guidance.mjs +15 -0
- package/src/owned-sources.mjs +479 -0
- package/src/service-templates.mjs +222 -19
- package/src/sync.mjs +57 -0
- package/src/ui-templates.mjs +180 -1
- package/src/validate.mjs +44 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appErrorSource,
|
|
3
|
+
applySchemaCliSource,
|
|
4
|
+
createRequestContextSource,
|
|
5
|
+
csrfSource,
|
|
6
|
+
firebaseSource,
|
|
7
|
+
httpErrorsSource,
|
|
8
|
+
mongoUsersSource,
|
|
9
|
+
rateLimitSource,
|
|
10
|
+
readEnvSource,
|
|
11
|
+
requestContextSource,
|
|
12
|
+
rpcHandlerSource,
|
|
13
|
+
rpcSource,
|
|
14
|
+
schemaLifecycleSource,
|
|
15
|
+
useCaseSource
|
|
16
|
+
} from "./service-templates.mjs";
|
|
17
|
+
import {
|
|
18
|
+
extensionAuthStoreSource,
|
|
19
|
+
notificationHostSource,
|
|
20
|
+
notificationStoreSource,
|
|
21
|
+
uiErrorMessagesSource,
|
|
22
|
+
uiErrorsSource,
|
|
23
|
+
uiHttpSource,
|
|
24
|
+
uiRpcSource
|
|
25
|
+
} from "./ui-templates.mjs";
|
|
26
|
+
|
|
27
|
+
const requestContextTestSource = `import type { Request } from "express";
|
|
28
|
+
import { verifyFirebaseToken } from "@/platform/auth/firebase";
|
|
29
|
+
import { createRequestContext } from "@/platform/http/createRequestContext";
|
|
30
|
+
|
|
31
|
+
jest.mock("@/platform/auth/firebase", () => ({ verifyFirebaseToken: jest.fn() }));
|
|
32
|
+
|
|
33
|
+
const verifyToken = verifyFirebaseToken as jest.MockedFunction<typeof verifyFirebaseToken>;
|
|
34
|
+
const request = (headers: Request["headers"] = {}) => ({ headers }) as Request;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => verifyToken.mockReset());
|
|
37
|
+
|
|
38
|
+
it("creates and caches an anonymous context with a bounded request id", async () => {
|
|
39
|
+
const req = request({ "x-request-id": "request.safe:1" });
|
|
40
|
+
const first = await createRequestContext(req);
|
|
41
|
+
const second = await createRequestContext(req);
|
|
42
|
+
expect(first).toEqual({ requestId: "request.safe:1" });
|
|
43
|
+
expect(second).toBe(first);
|
|
44
|
+
expect(verifyToken).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("replaces malformed and oversized request ids", async () => {
|
|
48
|
+
for (const value of ["contains spaces", "x".repeat(129)]) {
|
|
49
|
+
const context = await createRequestContext(request({ "x-request-id": value }));
|
|
50
|
+
expect(context.requestId).not.toBe(value);
|
|
51
|
+
expect(context.requestId).toMatch(/^[a-f0-9-]{36}$/);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("strictly parses one Bearer token", async () => {
|
|
56
|
+
for (const authorization of ["bearer token", "Bearer", "Bearer token", "Bearer token extra", "Basic token"]) {
|
|
57
|
+
await expect(createRequestContext(request({ authorization }))).rejects.toMatchObject({
|
|
58
|
+
code: "UNAUTHENTICATED",
|
|
59
|
+
kind: "unauthenticated"
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
expect(verifyToken).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("maps verified and unverified Firebase email claims and verifies only once", async () => {
|
|
66
|
+
verifyToken.mockResolvedValueOnce({ uid: "verified", email: "v@example.com", email_verified: true } as never);
|
|
67
|
+
const verifiedRequest = request({ authorization: "Bearer verified-token" });
|
|
68
|
+
const verified = await createRequestContext(verifiedRequest);
|
|
69
|
+
expect(verified.principal).toEqual({ uid: "verified", email: "v@example.com", emailVerified: true });
|
|
70
|
+
await createRequestContext(verifiedRequest);
|
|
71
|
+
expect(verifyToken).toHaveBeenCalledTimes(1);
|
|
72
|
+
|
|
73
|
+
verifyToken.mockResolvedValueOnce({ uid: "unverified", email: "u@example.com" } as never);
|
|
74
|
+
const unverified = await createRequestContext(request({ authorization: "Bearer unverified-token" }));
|
|
75
|
+
expect(unverified.principal).toEqual({ uid: "unverified", email: "u@example.com", emailVerified: false });
|
|
76
|
+
});
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const errorTranslationTestSource = `import { AppError, type FailureKind } from "@/core/errors";
|
|
80
|
+
import { errorHandler } from "@/platform/http/errors";
|
|
81
|
+
|
|
82
|
+
const statuses: Array<[FailureKind, number]> = [
|
|
83
|
+
["invalid", 400],
|
|
84
|
+
["unauthenticated", 401],
|
|
85
|
+
["forbidden", 403],
|
|
86
|
+
["not_found", 404],
|
|
87
|
+
["conflict", 409],
|
|
88
|
+
["unprocessable", 422],
|
|
89
|
+
["rate_limited", 429],
|
|
90
|
+
["unavailable", 503],
|
|
91
|
+
["internal", 500]
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
it.each(statuses)("translates %s to HTTP %i", (kind, status) => {
|
|
95
|
+
const json = jest.fn();
|
|
96
|
+
const response = { status: jest.fn(() => ({ json })) } as never;
|
|
97
|
+
errorHandler(new AppError("CODE", "message", kind), {} as never, response, jest.fn());
|
|
98
|
+
expect((response as { status: jest.Mock }).status).toHaveBeenCalledWith(status);
|
|
99
|
+
expect(json).toHaveBeenCalledWith({ error: { code: "CODE", message: "message" } });
|
|
100
|
+
});
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const rateLimitTestSource = `import type { Request } from "express";
|
|
104
|
+
import { RateLimiterRes } from "rate-limiter-flexible";
|
|
105
|
+
import { createRateLimiter } from "@/platform/http/rateLimit";
|
|
106
|
+
|
|
107
|
+
const mockConsume = jest.fn();
|
|
108
|
+
jest.mock("rate-limiter-flexible", () => {
|
|
109
|
+
class MockRateLimiterRes {}
|
|
110
|
+
return {
|
|
111
|
+
RateLimiterRes: MockRateLimiterRes,
|
|
112
|
+
RateLimiterRedis: jest.fn().mockImplementation(() => ({ consume: mockConsume }))
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const policy = { keyPrefix: "test", points: 2, durationSeconds: 60, key: "identity-or-ip" as const };
|
|
117
|
+
|
|
118
|
+
beforeEach(() => mockConsume.mockReset());
|
|
119
|
+
|
|
120
|
+
it("uses the authenticated identity and allows a request within budget", async () => {
|
|
121
|
+
mockConsume.mockResolvedValue(undefined);
|
|
122
|
+
const request = {
|
|
123
|
+
headers: {},
|
|
124
|
+
ip: "127.0.0.1",
|
|
125
|
+
requestContext: { requestId: "test", principal: { uid: "user-1", emailVerified: false } }
|
|
126
|
+
} as Request;
|
|
127
|
+
const next = jest.fn();
|
|
128
|
+
await (createRateLimiter({} as never, policy) as never as Function)(request, {}, next);
|
|
129
|
+
expect(mockConsume).toHaveBeenCalledWith("user:user-1");
|
|
130
|
+
expect(next).toHaveBeenCalledWith();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("translates exhausted and unavailable limiter failures", async () => {
|
|
134
|
+
const request = { headers: {}, ip: "127.0.0.1", requestContext: { requestId: "test" } } as Request;
|
|
135
|
+
const exhausted = jest.fn();
|
|
136
|
+
mockConsume.mockRejectedValueOnce(new RateLimiterRes());
|
|
137
|
+
await (createRateLimiter({} as never, policy) as never as Function)(request, {}, exhausted);
|
|
138
|
+
expect(exhausted.mock.calls[0]?.[0]).toMatchObject({ kind: "rate_limited", code: "RATE_LIMITED" });
|
|
139
|
+
|
|
140
|
+
const unavailable = jest.fn();
|
|
141
|
+
mockConsume.mockRejectedValueOnce(new Error("redis down"));
|
|
142
|
+
await (createRateLimiter({} as never, policy) as never as Function)(request, {}, unavailable);
|
|
143
|
+
expect(unavailable.mock.calls[0]?.[0]).toMatchObject({ kind: "unavailable", code: "RATE_LIMIT_UNAVAILABLE" });
|
|
144
|
+
});
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
const notificationTestSource = `import { mount } from "@vue/test-utils";
|
|
148
|
+
import { createPinia, setActivePinia } from "pinia";
|
|
149
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
150
|
+
import { nextTick } from "vue";
|
|
151
|
+
import NotificationHost from "@/app/components/NotificationHost.vue";
|
|
152
|
+
import { useNotificationStore } from "@/app/stores/notification.store";
|
|
153
|
+
|
|
154
|
+
describe("notification shell", () => {
|
|
155
|
+
beforeEach(() => {
|
|
156
|
+
vi.useFakeTimers();
|
|
157
|
+
setActivePinia(createPinia());
|
|
158
|
+
});
|
|
159
|
+
afterEach(() => vi.useRealTimers());
|
|
160
|
+
|
|
161
|
+
it("orders, dismisses, times, and clears notifications", async () => {
|
|
162
|
+
const store = useNotificationStore();
|
|
163
|
+
const host = mount(NotificationHost);
|
|
164
|
+
const first = store.notify({ message: "first", kind: "warning", durationMs: 1000 });
|
|
165
|
+
const second = store.notify({ message: "second", kind: "success", durationMs: 1000 });
|
|
166
|
+
expect(store.active?.id).toBe(first);
|
|
167
|
+
await nextTick();
|
|
168
|
+
expect(host.text()).toContain("first");
|
|
169
|
+
vi.advanceTimersByTime(1000);
|
|
170
|
+
await nextTick();
|
|
171
|
+
expect(store.active?.id).toBe(second);
|
|
172
|
+
store.dismiss(second);
|
|
173
|
+
expect(store.active).toBeUndefined();
|
|
174
|
+
store.notify({ message: "third" });
|
|
175
|
+
store.clear();
|
|
176
|
+
expect(store.queue).toEqual([]);
|
|
177
|
+
host.unmount();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
const flutterErrorsSource = `enum FailureKind {
|
|
183
|
+
invalid,
|
|
184
|
+
unauthenticated,
|
|
185
|
+
forbidden,
|
|
186
|
+
notFound,
|
|
187
|
+
conflict,
|
|
188
|
+
unprocessable,
|
|
189
|
+
rateLimited,
|
|
190
|
+
unavailable,
|
|
191
|
+
internal,
|
|
192
|
+
network,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
class ServiceException implements Exception {
|
|
196
|
+
const ServiceException({
|
|
197
|
+
required this.code,
|
|
198
|
+
required this.message,
|
|
199
|
+
required this.kind,
|
|
200
|
+
this.statusCode,
|
|
201
|
+
this.details,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
final String code;
|
|
205
|
+
final String message;
|
|
206
|
+
final FailureKind kind;
|
|
207
|
+
final int? statusCode;
|
|
208
|
+
final Object? details;
|
|
209
|
+
|
|
210
|
+
@override
|
|
211
|
+
String toString() => message;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
FailureKind failureKindForStatus(int? statusCode) {
|
|
215
|
+
if (statusCode == null) return FailureKind.network;
|
|
216
|
+
if (statusCode == 401) return FailureKind.unauthenticated;
|
|
217
|
+
if (statusCode == 403) return FailureKind.forbidden;
|
|
218
|
+
if (statusCode == 404) return FailureKind.notFound;
|
|
219
|
+
if (statusCode == 409) return FailureKind.conflict;
|
|
220
|
+
if (statusCode == 422) return FailureKind.unprocessable;
|
|
221
|
+
if (statusCode == 429) return FailureKind.rateLimited;
|
|
222
|
+
if (statusCode == 503) return FailureKind.unavailable;
|
|
223
|
+
if (statusCode >= 500) return FailureKind.internal;
|
|
224
|
+
return FailureKind.invalid;
|
|
225
|
+
}
|
|
226
|
+
`;
|
|
227
|
+
|
|
228
|
+
const flutterRpcSource = `typedef JsonObject = Map<String, dynamic>;
|
|
229
|
+
|
|
230
|
+
JsonObject? rpcErrorFrom(Object? value) {
|
|
231
|
+
if (value is! JsonObject) return null;
|
|
232
|
+
final error = value['error'];
|
|
233
|
+
return error is JsonObject ? error : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
T rpcResult<T>(Object? value) {
|
|
237
|
+
if (value is! JsonObject || !value.containsKey('result')) {
|
|
238
|
+
throw const FormatException('The server returned an invalid response.');
|
|
239
|
+
}
|
|
240
|
+
return value['result'] as T;
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
|
|
244
|
+
const flutterNotificationSource = `import 'dart:async';
|
|
245
|
+
|
|
246
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
247
|
+
|
|
248
|
+
enum NotificationKind { info, success, warning, error }
|
|
249
|
+
|
|
250
|
+
class AppNotification {
|
|
251
|
+
const AppNotification({
|
|
252
|
+
required this.id,
|
|
253
|
+
required this.message,
|
|
254
|
+
required this.kind,
|
|
255
|
+
required this.duration,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
final int id;
|
|
259
|
+
final String message;
|
|
260
|
+
final NotificationKind kind;
|
|
261
|
+
final Duration duration;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
class NotificationState {
|
|
265
|
+
const NotificationState(this.queue);
|
|
266
|
+
|
|
267
|
+
final List<AppNotification> queue;
|
|
268
|
+
AppNotification? get active => queue.isEmpty ? null : queue.first;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
class NotificationNotifier extends Notifier<NotificationState> {
|
|
272
|
+
Timer? _timer;
|
|
273
|
+
int _nextId = 1;
|
|
274
|
+
|
|
275
|
+
@override
|
|
276
|
+
NotificationState build() => const NotificationState([]);
|
|
277
|
+
|
|
278
|
+
void notify(
|
|
279
|
+
String message, {
|
|
280
|
+
NotificationKind kind = NotificationKind.info,
|
|
281
|
+
Duration duration = const Duration(seconds: 4),
|
|
282
|
+
}) {
|
|
283
|
+
final notification = AppNotification(
|
|
284
|
+
id: _nextId++,
|
|
285
|
+
message: message,
|
|
286
|
+
kind: kind,
|
|
287
|
+
duration: duration,
|
|
288
|
+
);
|
|
289
|
+
final wasEmpty = state.queue.isEmpty;
|
|
290
|
+
state = NotificationState([...state.queue, notification]);
|
|
291
|
+
if (wasEmpty) _scheduleActive();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
void dismiss(int id) {
|
|
295
|
+
final wasActive = state.active?.id == id;
|
|
296
|
+
state = NotificationState(
|
|
297
|
+
state.queue.where((notification) => notification.id != id).toList(),
|
|
298
|
+
);
|
|
299
|
+
if (wasActive) _scheduleActive();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
void clear() {
|
|
303
|
+
_timer?.cancel();
|
|
304
|
+
_timer = null;
|
|
305
|
+
state = const NotificationState([]);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
void _scheduleActive() {
|
|
309
|
+
_timer?.cancel();
|
|
310
|
+
_timer = null;
|
|
311
|
+
final notification = state.active;
|
|
312
|
+
if (notification != null && notification.duration > Duration.zero) {
|
|
313
|
+
_timer = Timer(notification.duration, () => dismiss(notification.id));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
final notificationProvider =
|
|
319
|
+
NotifierProvider<NotificationNotifier, NotificationState>(
|
|
320
|
+
NotificationNotifier.new,
|
|
321
|
+
);
|
|
322
|
+
`;
|
|
323
|
+
|
|
324
|
+
const flutterNotificationHostSource = `import 'package:flutter/material.dart';
|
|
325
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
326
|
+
|
|
327
|
+
import '../state/notification_notifier.dart';
|
|
328
|
+
|
|
329
|
+
class NotificationHost extends ConsumerWidget {
|
|
330
|
+
const NotificationHost({super.key});
|
|
331
|
+
|
|
332
|
+
@override
|
|
333
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
334
|
+
final notification = ref.watch(notificationProvider).active;
|
|
335
|
+
if (notification == null) return const SizedBox.shrink();
|
|
336
|
+
final colors = Theme.of(context).colorScheme;
|
|
337
|
+
final background = switch (notification.kind) {
|
|
338
|
+
NotificationKind.info => colors.primary,
|
|
339
|
+
NotificationKind.success => Colors.green.shade800,
|
|
340
|
+
NotificationKind.warning => Colors.orange.shade900,
|
|
341
|
+
NotificationKind.error => colors.error,
|
|
342
|
+
};
|
|
343
|
+
return Semantics(
|
|
344
|
+
liveRegion: true,
|
|
345
|
+
child: Material(
|
|
346
|
+
color: background,
|
|
347
|
+
borderRadius: BorderRadius.circular(12),
|
|
348
|
+
elevation: 6,
|
|
349
|
+
child: Padding(
|
|
350
|
+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
351
|
+
child: Row(
|
|
352
|
+
mainAxisSize: MainAxisSize.min,
|
|
353
|
+
children: [
|
|
354
|
+
Flexible(
|
|
355
|
+
child: Text(
|
|
356
|
+
notification.message,
|
|
357
|
+
style: const TextStyle(color: Colors.white),
|
|
358
|
+
),
|
|
359
|
+
),
|
|
360
|
+
IconButton(
|
|
361
|
+
tooltip: 'Dismiss notification',
|
|
362
|
+
color: Colors.white,
|
|
363
|
+
onPressed: () => ref
|
|
364
|
+
.read(notificationProvider.notifier)
|
|
365
|
+
.dismiss(notification.id),
|
|
366
|
+
icon: const Icon(Icons.close),
|
|
367
|
+
),
|
|
368
|
+
],
|
|
369
|
+
),
|
|
370
|
+
),
|
|
371
|
+
),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
`;
|
|
376
|
+
|
|
377
|
+
const flutterNotificationsPublicSource = `export '../state/notification_notifier.dart'
|
|
378
|
+
show
|
|
379
|
+
NotificationKind,
|
|
380
|
+
NotificationNotifier,
|
|
381
|
+
NotificationState,
|
|
382
|
+
notificationProvider;
|
|
383
|
+
`;
|
|
384
|
+
|
|
385
|
+
const flutterNotificationTestSource = (packageName) => `import 'package:${packageName}/app/public/notifications.dart';
|
|
386
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
387
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
388
|
+
|
|
389
|
+
void main() {
|
|
390
|
+
testWidgets('orders, dismisses, times, and clears notifications', (
|
|
391
|
+
tester,
|
|
392
|
+
) async {
|
|
393
|
+
final container = ProviderContainer();
|
|
394
|
+
addTearDown(container.dispose);
|
|
395
|
+
final notifier = container.read(notificationProvider.notifier);
|
|
396
|
+
|
|
397
|
+
notifier.notify(
|
|
398
|
+
'first',
|
|
399
|
+
kind: NotificationKind.warning,
|
|
400
|
+
duration: const Duration(milliseconds: 10),
|
|
401
|
+
);
|
|
402
|
+
notifier.notify(
|
|
403
|
+
'second',
|
|
404
|
+
kind: NotificationKind.success,
|
|
405
|
+
duration: Duration.zero,
|
|
406
|
+
);
|
|
407
|
+
expect(container.read(notificationProvider).active?.message, 'first');
|
|
408
|
+
|
|
409
|
+
await tester.pump(const Duration(milliseconds: 11));
|
|
410
|
+
expect(container.read(notificationProvider).active?.message, 'second');
|
|
411
|
+
final second = container.read(notificationProvider).active!;
|
|
412
|
+
notifier.dismiss(second.id);
|
|
413
|
+
expect(container.read(notificationProvider).active, isNull);
|
|
414
|
+
|
|
415
|
+
notifier.notify('third', duration: Duration.zero);
|
|
416
|
+
notifier.clear();
|
|
417
|
+
expect(container.read(notificationProvider).queue, isEmpty);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
`;
|
|
421
|
+
|
|
422
|
+
export function ownedSources(config, { flutterPackageName } = {}) {
|
|
423
|
+
const profiles = new Set(config.profiles);
|
|
424
|
+
if (config.kind === "service") {
|
|
425
|
+
const files = {
|
|
426
|
+
"src/core/RequestContext.ts": requestContextSource,
|
|
427
|
+
"src/core/UseCase.ts": useCaseSource,
|
|
428
|
+
"src/core/errors.ts": appErrorSource,
|
|
429
|
+
"src/core/SchemaLifecycle.ts": schemaLifecycleSource,
|
|
430
|
+
"src/platform/config/readEnv.ts": readEnvSource,
|
|
431
|
+
"src/platform/http/createRequestContext.ts": createRequestContextSource(profiles.has("firebase") ? "firebase" : "none"),
|
|
432
|
+
"src/platform/http/csrf.ts": csrfSource,
|
|
433
|
+
"src/platform/http/errors.ts": httpErrorsSource,
|
|
434
|
+
"src/platform/http/rpc.ts": rpcSource,
|
|
435
|
+
"src/platform/http/rpcHandler.ts": rpcHandlerSource,
|
|
436
|
+
"src/app/cli/applySchema.ts": applySchemaCliSource,
|
|
437
|
+
"src/platform/http/__tests__/errors.test.ts": errorTranslationTestSource
|
|
438
|
+
};
|
|
439
|
+
if (profiles.has("firebase")) {
|
|
440
|
+
files["src/platform/auth/firebase.ts"] = firebaseSource;
|
|
441
|
+
files["src/platform/http/__tests__/createRequestContext.test.ts"] = requestContextTestSource;
|
|
442
|
+
}
|
|
443
|
+
if (profiles.has("rate-limit")) {
|
|
444
|
+
files["src/platform/http/rateLimit.ts"] = rateLimitSource;
|
|
445
|
+
files["src/platform/http/__tests__/rateLimit.test.ts"] = rateLimitTestSource;
|
|
446
|
+
}
|
|
447
|
+
if (profiles.has("mongo")) files["deploy/mongo/10-create-users.js"] = mongoUsersSource;
|
|
448
|
+
return files;
|
|
449
|
+
}
|
|
450
|
+
if (config.kind === "ui" || config.kind === "extension") {
|
|
451
|
+
const files = {
|
|
452
|
+
"src/core/errors.ts": uiErrorsSource,
|
|
453
|
+
"src/core/rpc.ts": uiRpcSource,
|
|
454
|
+
"src/platform/errors.ts": uiErrorMessagesSource,
|
|
455
|
+
"src/platform/http.ts": uiHttpSource
|
|
456
|
+
};
|
|
457
|
+
if (config.kind === "extension" && profiles.has("firebase")) {
|
|
458
|
+
files["src/app/stores/auth.store.ts"] = extensionAuthStoreSource;
|
|
459
|
+
}
|
|
460
|
+
if (profiles.has("notifications")) {
|
|
461
|
+
files["src/app/stores/notification.store.ts"] = notificationStoreSource;
|
|
462
|
+
files["src/app/components/NotificationHost.vue"] = notificationHostSource;
|
|
463
|
+
files["src/app/__tests__/notificationStore.test.ts"] = notificationTestSource;
|
|
464
|
+
}
|
|
465
|
+
return files;
|
|
466
|
+
}
|
|
467
|
+
const files = {
|
|
468
|
+
"lib/core/errors.dart": flutterErrorsSource,
|
|
469
|
+
"lib/core/rpc.dart": flutterRpcSource
|
|
470
|
+
};
|
|
471
|
+
if (profiles.has("notifications")) {
|
|
472
|
+
if (!flutterPackageName) throw new Error("Flutter notification sources require a package name");
|
|
473
|
+
files["lib/app/state/notification_notifier.dart"] = flutterNotificationSource;
|
|
474
|
+
files["lib/app/components/notification_host.dart"] = flutterNotificationHostSource;
|
|
475
|
+
files["lib/app/public/notifications.dart"] = flutterNotificationsPublicSource;
|
|
476
|
+
files["test/notification_notifier_test.dart"] = flutterNotificationTestSource(flutterPackageName);
|
|
477
|
+
}
|
|
478
|
+
return files;
|
|
479
|
+
}
|