@suucha/core 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Suucha Studio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @suucha/core
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../../LICENSE)
4
+
5
+ Platform-agnostic core abstractions for the Suucha Web SDK. Defines reporter / interceptor / logger / storage / ad-manager interfaces and exposes the `suucha` singleton.
6
+
7
+ This package has **no browser or Node.js dependencies** and can be reused by any platform (Web, React Native, mini-program).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @suucha/core
13
+ # or
14
+ npm install @suucha/core
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```typescript
20
+ import { suucha, LogLevel } from '@suucha/core';
21
+
22
+ // Register a reporter (third-party implementation)
23
+ class ConsoleReporter {
24
+ readonly name = 'console';
25
+ async logEvent(name: string, params: Record<string, any>) {
26
+ console.log('[event]', name, params);
27
+ }
28
+ }
29
+
30
+ suucha.addReporter(new ConsoleReporter());
31
+
32
+ // Log events — anonymous_id is auto-attached
33
+ suucha.log('page_view');
34
+ suucha.log('add_to_cart', { item_id: 'sku_123', value: 99.9 });
35
+
36
+ // Identify logged-in user — user_id is auto-attached on subsequent events
37
+ suucha.identify('user_456');
38
+ ```
39
+
40
+ ## Key Concepts
41
+
42
+ ### Reporters
43
+ Implement `ILogEventReporter` and register via `suucha.addReporter(...)`. Each reporter represents one third-party service (GA4, Sensors, etc.).
44
+
45
+ ### Interceptors
46
+ - **Event interceptors** (`ILogEventIntercept`) can produce *new* events (e.g., event counting).
47
+ - **Parameter interceptors** (`ILogEventParameterIntercept`) can modify event params (e.g., attribution, identity).
48
+
49
+ Built-in:
50
+ - `IdentityIntercept` — auto-attached, adds `anonymous_id` and `user_id`
51
+ - `EventCounterIntercept` — count events, emit `event_NNN` at configured counts
52
+
53
+ ### Region Routing
54
+ For domestic/overseas reporter switching:
55
+
56
+ ```typescript
57
+ suucha.region.router((ctx) => {
58
+ if (ctx.host.endsWith('.cn')) return 'cn';
59
+ return 'global';
60
+ });
61
+
62
+ suucha.region.reporters({
63
+ cn: [/* SensorsReporter */],
64
+ global: [/* GA4Reporter */],
65
+ });
66
+ ```
67
+
68
+ ### Consent (Privacy)
69
+ GDPR / PIPL compliance:
70
+
71
+ ```typescript
72
+ suucha.setConsent(false); // events buffer in memory
73
+ // ... user grants consent ...
74
+ suucha.setConsent(true); // buffered events flush to reporters
75
+ ```
76
+
77
+ ## API
78
+
79
+ See [`docs/api-draft.md`](../../docs/api-draft.md) for the full v1 API contract.
80
+
81
+ ## License
82
+
83
+ MIT © Suucha Studio
package/dist/index.cjs ADDED
@@ -0,0 +1,292 @@
1
+ 'use strict';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
6
+
7
+ // src/types.ts
8
+ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
9
+ LogLevel2[LogLevel2["Trace"] = 0] = "Trace";
10
+ LogLevel2[LogLevel2["Debug"] = 1] = "Debug";
11
+ LogLevel2[LogLevel2["Information"] = 2] = "Information";
12
+ LogLevel2[LogLevel2["Warning"] = 3] = "Warning";
13
+ LogLevel2[LogLevel2["Error"] = 4] = "Error";
14
+ LogLevel2[LogLevel2["Critical"] = 5] = "Critical";
15
+ LogLevel2[LogLevel2["None"] = 6] = "None";
16
+ return LogLevel2;
17
+ })(LogLevel || {});
18
+
19
+ // src/interceptors.ts
20
+ var IdentityIntercept = class {
21
+ constructor(getAnonymousId, getUserId) {
22
+ __publicField(this, "getAnonymousId", getAnonymousId);
23
+ __publicField(this, "getUserId", getUserId);
24
+ __publicField(this, "name", "suucha-identity");
25
+ __publicField(this, "order", 100);
26
+ }
27
+ async execute(_eventName, params) {
28
+ const enriched = {
29
+ ...params,
30
+ anonymous_id: this.getAnonymousId()
31
+ };
32
+ const userId = this.getUserId();
33
+ if (userId) {
34
+ enriched.user_id = userId;
35
+ }
36
+ return enriched;
37
+ }
38
+ };
39
+ var EventCounterIntercept = class {
40
+ constructor(configurations) {
41
+ __publicField(this, "configurations", configurations);
42
+ __publicField(this, "name", "suucha-event-counter");
43
+ __publicField(this, "order", 200);
44
+ __publicField(this, "counts", /* @__PURE__ */ new Map());
45
+ }
46
+ async execute(eventName, params) {
47
+ const events = [];
48
+ for (const config of this.configurations) {
49
+ const isMatch = eventName === config.name || eventName.startsWith(`${config.name}_`);
50
+ if (!isMatch) continue;
51
+ const current = (this.counts.get(config.name) ?? 0) + 1;
52
+ this.counts.set(config.name, current);
53
+ if (config.counts.includes(current)) {
54
+ const padLength = config.padLength ?? 3;
55
+ const padded = String(current).padStart(padLength, "0");
56
+ events.push({
57
+ name: `${config.name}_${padded}`,
58
+ params: { ...params }
59
+ });
60
+ }
61
+ }
62
+ return { events };
63
+ }
64
+ };
65
+
66
+ // src/suucha.ts
67
+ var DefaultLogger = class {
68
+ log(_level, _message, ..._args) {
69
+ }
70
+ };
71
+ var DEFAULT_REGION = "default";
72
+ var Suucha = class {
73
+ constructor() {
74
+ __publicField(this, "reporters", /* @__PURE__ */ new Map());
75
+ __publicField(this, "eventIntercepts", []);
76
+ __publicField(this, "paramIntercepts", []);
77
+ __publicField(this, "logger", new DefaultLogger());
78
+ __publicField(this, "logLevel", 2 /* Information */);
79
+ __publicField(this, "anonymousId");
80
+ __publicField(this, "userId", null);
81
+ __publicField(this, "consent", true);
82
+ __publicField(this, "consentQueue", []);
83
+ __publicField(this, "regionFn");
84
+ __publicField(this, "regionReportersMap", {});
85
+ __publicField(this, "currentRegion", DEFAULT_REGION);
86
+ __publicField(this, "resolvedRegionReporters", []);
87
+ __publicField(this, "regionContext", null);
88
+ __publicField(this, "adManagerValue", null);
89
+ __publicField(this, "storageValue");
90
+ __publicField(this, "region");
91
+ this.anonymousId = this.generateAnonymousId();
92
+ this.paramIntercepts.push(
93
+ new IdentityIntercept(() => this.anonymousId, () => this.userId)
94
+ );
95
+ this.region = {
96
+ router: (fn) => {
97
+ this.regionFn = fn;
98
+ this.resolveRegion();
99
+ },
100
+ reporters: (map) => {
101
+ this.regionReportersMap = map;
102
+ this.resolveRegion();
103
+ },
104
+ getRegion: () => this.currentRegion,
105
+ getReporters: () => this.getActiveReporters()
106
+ };
107
+ }
108
+ // ===== 日志 =====
109
+ setLogLevel(level) {
110
+ this.logLevel = level;
111
+ }
112
+ getLogLevel() {
113
+ return this.logLevel;
114
+ }
115
+ setLogger(logger) {
116
+ this.logger = logger;
117
+ }
118
+ getLogger() {
119
+ return this.logger;
120
+ }
121
+ // ===== 埋点 =====
122
+ async log(eventName, params = {}) {
123
+ let enriched = { ...params };
124
+ for (const ic of this.paramIntercepts) {
125
+ enriched = await ic.execute(eventName, enriched);
126
+ }
127
+ const allEvents = [
128
+ { name: eventName, params: enriched }
129
+ ];
130
+ for (const ic of this.eventIntercepts) {
131
+ const result = await ic.execute(eventName, enriched);
132
+ if (result.events) {
133
+ allEvents.push(...result.events);
134
+ }
135
+ }
136
+ if (!this.consent) {
137
+ for (const ev of allEvents) {
138
+ this.consentQueue.push(ev);
139
+ }
140
+ return;
141
+ }
142
+ await this.dispatch(allEvents);
143
+ }
144
+ addReporter(reporter) {
145
+ this.reporters.set(reporter.name, reporter);
146
+ }
147
+ removeReporter(name) {
148
+ this.reporters.delete(name);
149
+ }
150
+ getReporters() {
151
+ return this.getActiveReporters();
152
+ }
153
+ // ===== 拦截器 =====
154
+ addEventIntercept(ic) {
155
+ this.eventIntercepts.push(ic);
156
+ this.eventIntercepts.sort((a, b) => a.order - b.order);
157
+ }
158
+ addParameterIntercept(ic) {
159
+ this.paramIntercepts.push(ic);
160
+ this.paramIntercepts.sort((a, b) => a.order - b.order);
161
+ }
162
+ removeIntercept(name) {
163
+ const ei = this.eventIntercepts.findIndex((x) => x.name === name);
164
+ if (ei >= 0) this.eventIntercepts.splice(ei, 1);
165
+ const pi = this.paramIntercepts.findIndex((x) => x.name === name);
166
+ if (pi >= 0) this.paramIntercepts.splice(pi, 1);
167
+ }
168
+ // ===== 用户身份 =====
169
+ identify(userId, traits) {
170
+ this.userId = userId;
171
+ if (traits) {
172
+ this.log("user_identify", { user_id: userId, ...traits }).catch(() => {
173
+ });
174
+ }
175
+ }
176
+ getAnonymousId() {
177
+ return this.anonymousId;
178
+ }
179
+ setAnonymousId(id) {
180
+ this.anonymousId = id;
181
+ }
182
+ getUserId() {
183
+ return this.userId;
184
+ }
185
+ reset() {
186
+ this.userId = null;
187
+ this.anonymousId = this.generateAnonymousId();
188
+ }
189
+ // ===== Consent =====
190
+ setConsent(granted) {
191
+ const wasGranted = this.consent;
192
+ this.consent = granted;
193
+ if (granted && !wasGranted) {
194
+ const queue = [...this.consentQueue];
195
+ this.consentQueue.length = 0;
196
+ for (const ev of queue) {
197
+ this.dispatch([ev]).catch(() => {
198
+ });
199
+ }
200
+ }
201
+ }
202
+ hasConsent() {
203
+ return this.consent;
204
+ }
205
+ // ===== Storage =====
206
+ setStorage(storage) {
207
+ this.storageValue = storage;
208
+ }
209
+ getStorage() {
210
+ return this.storageValue;
211
+ }
212
+ // ===== Region Context(由平台层调用) =====
213
+ /**
214
+ * 平台层(如 @suucha/web 的 setupWeb)调用此方法注入 region 上下文。
215
+ * 注入后会立即运行 router 函数确定 region。
216
+ */
217
+ setRegionContext(ctx) {
218
+ this.regionContext = ctx;
219
+ this.resolveRegion();
220
+ }
221
+ getRegionContext() {
222
+ return this.regionContext;
223
+ }
224
+ // ===== 广告 =====
225
+ enableAds(manager) {
226
+ this.adManagerValue = manager;
227
+ return manager;
228
+ }
229
+ getAdManager() {
230
+ return this.adManagerValue;
231
+ }
232
+ // ===== 内部方法 =====
233
+ getActiveReporters() {
234
+ if (this.resolvedRegionReporters.length > 0) {
235
+ return this.resolvedRegionReporters;
236
+ }
237
+ return [...this.reporters.values()];
238
+ }
239
+ resolveRegion() {
240
+ if (this.regionFn && this.regionContext) {
241
+ const newRegion = this.regionFn(this.regionContext);
242
+ if (newRegion !== this.currentRegion) {
243
+ this.currentRegion = newRegion;
244
+ }
245
+ }
246
+ const provider = this.regionReportersMap[this.currentRegion];
247
+ if (Array.isArray(provider)) {
248
+ this.resolvedRegionReporters = provider;
249
+ } else if (typeof provider === "function") {
250
+ this.resolvedRegionReporters = [];
251
+ provider().then((rs) => {
252
+ this.resolvedRegionReporters = rs;
253
+ }).catch((err) => {
254
+ this.logger.log(
255
+ 3 /* Warning */,
256
+ `[Suucha] Failed to load reporters for region "${this.currentRegion}":`,
257
+ err
258
+ );
259
+ });
260
+ }
261
+ }
262
+ async dispatch(events) {
263
+ const reporters = this.getActiveReporters();
264
+ if (reporters.length === 0) return;
265
+ await Promise.all(
266
+ events.flatMap(
267
+ (ev) => reporters.map(
268
+ (r) => r.logEvent(ev.name, ev.params).catch((err) => {
269
+ this.logger.log(
270
+ 4 /* Error */,
271
+ `[Suucha] Reporter "${r.name}" failed to log "${ev.name}":`,
272
+ err
273
+ );
274
+ })
275
+ )
276
+ )
277
+ );
278
+ }
279
+ generateAnonymousId() {
280
+ const rand = Math.random().toString(36).slice(2, 10);
281
+ return `anon_${Date.now().toString(36)}_${rand}`;
282
+ }
283
+ };
284
+ var suucha = new Suucha();
285
+
286
+ exports.EventCounterIntercept = EventCounterIntercept;
287
+ exports.IdentityIntercept = IdentityIntercept;
288
+ exports.LogLevel = LogLevel;
289
+ exports.Suucha = Suucha;
290
+ exports.suucha = suucha;
291
+ //# sourceMappingURL=index.cjs.map
292
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/interceptors.ts","../src/suucha.ts"],"names":["LogLevel"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\r\n * @suucha/core - Type Definitions\r\n *\r\n * 平台无关的核心接口定义。\r\n * 此文件不依赖任何浏览器或 Node.js API,可被任何平台(Web、RN、小程序)复用。\r\n */\r\n\r\n// ===== 日志等级 =====\r\n\r\nexport enum LogLevel {\r\n Trace = 0,\r\n Debug = 1,\r\n Information = 2,\r\n Warning = 3,\r\n Error = 4,\r\n Critical = 5,\r\n None = 6,\r\n}\r\n\r\n// ===== 日志事件相关 =====\r\n\r\nexport interface LogEvent {\r\n name: string;\r\n params: Record<string, any>;\r\n timestamp: number;\r\n}\r\n\r\n/**\r\n * 日志事件上报器接口(第三方实现)\r\n *\r\n * 每个第三方服务(GA4、神策、Mixpanel 等)实现此接口,\r\n * 通过 `suucha.addReporter(...)` 或 `suucha.region.reporters(...)` 注册。\r\n */\r\nexport interface ILogEventReporter {\r\n readonly name: string;\r\n logEvent(name: string, params: Record<string, any>): Promise<void>;\r\n}\r\n\r\n/**\r\n * 事件拦截器:可产生新事件\r\n *\r\n * 用例:事件计数(spin 第 10 次自动产生 spin_010 事件)\r\n */\r\nexport interface ILogEventIntercept {\r\n readonly name: string;\r\n /** 越小越先执行 */\r\n readonly order: number;\r\n execute(\r\n eventName: string,\r\n params: Record<string, any>,\r\n ): Promise<{\r\n events?: Array<{ name: string; params: Record<string, any> }>;\r\n }>;\r\n}\r\n\r\n/**\r\n * 事件参数拦截器:可修改事件参数\r\n *\r\n * 用例:附加归因、用户身份、运行时长\r\n */\r\nexport interface ILogEventParameterIntercept {\r\n readonly name: string;\r\n /** 越小越先执行 */\r\n readonly order: number;\r\n execute(\r\n eventName: string,\r\n params: Record<string, any>,\r\n ): Promise<Record<string, any>>;\r\n}\r\n\r\n// ===== Logger =====\r\n\r\nexport interface ILogger {\r\n log(level: LogLevel, message: string, ...args: any[]): void;\r\n}\r\n\r\n// ===== Storage =====\r\n\r\nexport interface IStorage {\r\n get<T = any>(key: string): Promise<T | null>;\r\n set<T = any>(key: string, value: T, ttl?: number): Promise<void>;\r\n remove(key: string): Promise<void>;\r\n}\r\n\r\n// ===== 广告 =====\r\n\r\nexport interface AdSlotOptions {\r\n container: string | HTMLElement;\r\n slot?: string;\r\n format?: 'auto' | 'horizontal' | 'vertical' | 'rectangle';\r\n responsive?: boolean;\r\n}\r\n\r\nexport interface AdSlotHandle {\r\n readonly id: string;\r\n readonly slot: string;\r\n destroy(): void;\r\n}\r\n\r\nexport interface IAdManager {\r\n mountSlot(options: AdSlotOptions): Promise<AdSlotHandle>;\r\n unmountSlot(handle: AdSlotHandle): void;\r\n refresh?(handle: AdSlotHandle): void;\r\n}\r\n\r\n// ===== Consent =====\r\n\r\nexport interface IConsentManager {\r\n hasConsent(): boolean;\r\n onConsentChange(cb: (granted: boolean) => void): () => void;\r\n}\r\n\r\n// ===== Region 路由 =====\r\n\r\nexport interface RegionContext {\r\n host: string;\r\n pathname: string;\r\n timezone: string;\r\n language: string;\r\n url: string;\r\n}\r\n\r\nexport type ReporterProvider = ILogEventReporter[] | (() => Promise<ILogEventReporter[]>);\r\n\r\nexport interface RegionRouter {\r\n router(fn: (ctx: RegionContext) => string): void;\r\n reporters(map: Record<string, ReporterProvider>): void;\r\n getRegion(): string;\r\n getReporters(): ILogEventReporter[];\r\n}\r\n","/**\r\n * @suucha/core - 内置拦截器\r\n */\r\n\r\nimport type {\r\n ILogEventIntercept,\r\n ILogEventParameterIntercept,\r\n} from './types.js';\r\n\r\n/**\r\n * 事件计数配置\r\n */\r\nexport interface EventCounterConfiguration {\r\n /** 事件名或前缀 */\r\n name: string;\r\n /** 触发的次数列表 */\r\n counts: number[];\r\n /** 名称补 0 长度,默认 3 */\r\n padLength?: number;\r\n}\r\n\r\n/**\r\n * 用户身份拦截器(自动启用)\r\n * 给每个事件附加 anonymous_id 和(如果已 identify)user_id\r\n */\r\nexport class IdentityIntercept implements ILogEventParameterIntercept {\r\n readonly name = 'suucha-identity';\r\n readonly order = 100;\r\n\r\n constructor(\r\n private readonly getAnonymousId: () => string,\r\n private readonly getUserId: () => string | null,\r\n ) {}\r\n\r\n async execute(\r\n _eventName: string,\r\n params: Record<string, any>,\r\n ): Promise<Record<string, any>> {\r\n const enriched: Record<string, any> = {\r\n ...params,\r\n anonymous_id: this.getAnonymousId(),\r\n };\r\n const userId = this.getUserId();\r\n if (userId) {\r\n enriched.user_id = userId;\r\n }\r\n return enriched;\r\n }\r\n}\r\n\r\n/**\r\n * 事件计数拦截器\r\n *\r\n * 在配置的事件达到指定次数时,产生新事件(名称格式 {eventName}_{paddedCount})\r\n */\r\nexport class EventCounterIntercept implements ILogEventIntercept {\r\n readonly name = 'suucha-event-counter';\r\n readonly order = 200;\r\n\r\n private readonly counts = new Map<string, number>();\r\n\r\n constructor(private readonly configurations: EventCounterConfiguration[]) {}\r\n\r\n async execute(eventName: string, params: Record<string, any>) {\r\n const events: Array<{ name: string; params: Record<string, any> }> = [];\r\n\r\n for (const config of this.configurations) {\r\n const isMatch =\r\n eventName === config.name || eventName.startsWith(`${config.name}_`);\r\n if (!isMatch) continue;\r\n\r\n const current = (this.counts.get(config.name) ?? 0) + 1;\r\n this.counts.set(config.name, current);\r\n\r\n if (config.counts.includes(current)) {\r\n const padLength = config.padLength ?? 3;\r\n const padded = String(current).padStart(padLength, '0');\r\n events.push({\r\n name: `${config.name}_${padded}`,\r\n params: { ...params },\r\n });\r\n }\r\n }\r\n\r\n return { events };\r\n }\r\n}\r\n","/**\r\n * @suucha/core - Suucha 主类与单例\r\n */\r\n\r\nimport {\r\n IdentityIntercept,\r\n} from './interceptors.js';\r\nimport { LogLevel } from './types.js';\r\nimport type {\r\n ILogEventIntercept,\r\n ILogEventParameterIntercept,\r\n ILogEventReporter,\r\n ILogger,\r\n IStorage,\r\n IAdManager,\r\n RegionContext,\r\n RegionRouter,\r\n ReporterProvider,\r\n} from './types.js';\r\n\r\n/**\r\n * 默认 no-op logger,未配置 logger 时使用\r\n */\r\nclass DefaultLogger implements ILogger {\r\n log(_level: LogLevel, _message: string, ..._args: any[]): void {\r\n // no-op\r\n }\r\n}\r\n\r\nconst DEFAULT_REGION = 'default';\r\n\r\nexport class Suucha {\r\n private readonly reporters = new Map<string, ILogEventReporter>();\r\n private readonly eventIntercepts: ILogEventIntercept[] = [];\r\n private readonly paramIntercepts: ILogEventParameterIntercept[] = [];\r\n private logger: ILogger = new DefaultLogger();\r\n private logLevel: LogLevel = LogLevel.Information;\r\n private anonymousId: string;\r\n private userId: string | null = null;\r\n private consent = true;\r\n private readonly consentQueue: Array<{ name: string; params: Record<string, any> }> = [];\r\n private regionFn?: (ctx: RegionContext) => string;\r\n private regionReportersMap: Record<string, ReporterProvider> = {};\r\n private currentRegion = DEFAULT_REGION;\r\n private resolvedRegionReporters: ILogEventReporter[] = [];\r\n private regionContext: RegionContext | null = null;\r\n private adManagerValue: IAdManager | null = null;\r\n private storageValue?: IStorage;\r\n\r\n readonly region: RegionRouter;\r\n\r\n constructor() {\r\n this.anonymousId = this.generateAnonymousId();\r\n this.paramIntercepts.push(\r\n new IdentityIntercept(() => this.anonymousId, () => this.userId),\r\n );\r\n\r\n this.region = {\r\n router: (fn) => {\r\n this.regionFn = fn;\r\n this.resolveRegion();\r\n },\r\n reporters: (map) => {\r\n this.regionReportersMap = map;\r\n this.resolveRegion();\r\n },\r\n getRegion: () => this.currentRegion,\r\n getReporters: () => this.getActiveReporters(),\r\n };\r\n }\r\n\r\n // ===== 日志 =====\r\n\r\n setLogLevel(level: LogLevel): void {\r\n this.logLevel = level;\r\n }\r\n\r\n getLogLevel(): LogLevel {\r\n return this.logLevel;\r\n }\r\n\r\n setLogger(logger: ILogger): void {\r\n this.logger = logger;\r\n }\r\n\r\n getLogger(): ILogger {\r\n return this.logger;\r\n }\r\n\r\n // ===== 埋点 =====\r\n\r\n async log(eventName: string, params: Record<string, any> = {}): Promise<void> {\r\n // 1. 参数拦截器链\r\n let enriched = { ...params };\r\n for (const ic of this.paramIntercepts) {\r\n enriched = await ic.execute(eventName, enriched);\r\n }\r\n\r\n // 2. 事件拦截器链(可产生新事件)\r\n const allEvents: Array<{ name: string; params: Record<string, any> }> = [\r\n { name: eventName, params: enriched },\r\n ];\r\n for (const ic of this.eventIntercepts) {\r\n const result = await ic.execute(eventName, enriched);\r\n if (result.events) {\r\n allEvents.push(...result.events);\r\n }\r\n }\r\n\r\n // 3. Consent 检查:未同意则入队,不外发\r\n if (!this.consent) {\r\n for (const ev of allEvents) {\r\n this.consentQueue.push(ev);\r\n }\r\n return;\r\n }\r\n\r\n // 4. 分发到 reporters\r\n await this.dispatch(allEvents);\r\n }\r\n\r\n addReporter(reporter: ILogEventReporter): void {\r\n this.reporters.set(reporter.name, reporter);\r\n }\r\n\r\n removeReporter(name: string): void {\r\n this.reporters.delete(name);\r\n }\r\n\r\n getReporters(): ILogEventReporter[] {\r\n return this.getActiveReporters();\r\n }\r\n\r\n // ===== 拦截器 =====\r\n\r\n addEventIntercept(ic: ILogEventIntercept): void {\r\n this.eventIntercepts.push(ic);\r\n this.eventIntercepts.sort((a, b) => a.order - b.order);\r\n }\r\n\r\n addParameterIntercept(ic: ILogEventParameterIntercept): void {\r\n this.paramIntercepts.push(ic);\r\n this.paramIntercepts.sort((a, b) => a.order - b.order);\r\n }\r\n\r\n removeIntercept(name: string): void {\r\n const ei = this.eventIntercepts.findIndex((x) => x.name === name);\r\n if (ei >= 0) this.eventIntercepts.splice(ei, 1);\r\n const pi = this.paramIntercepts.findIndex((x) => x.name === name);\r\n if (pi >= 0) this.paramIntercepts.splice(pi, 1);\r\n }\r\n\r\n // ===== 用户身份 =====\r\n\r\n identify(userId: string, traits?: Record<string, any>): void {\r\n this.userId = userId;\r\n if (traits) {\r\n this.log('user_identify', { user_id: userId, ...traits }).catch(() => {});\r\n }\r\n }\r\n\r\n getAnonymousId(): string {\r\n return this.anonymousId;\r\n }\r\n\r\n setAnonymousId(id: string): void {\r\n this.anonymousId = id;\r\n }\r\n\r\n getUserId(): string | null {\r\n return this.userId;\r\n }\r\n\r\n reset(): void {\r\n this.userId = null;\r\n this.anonymousId = this.generateAnonymousId();\r\n }\r\n\r\n // ===== Consent =====\r\n\r\n setConsent(granted: boolean): void {\r\n const wasGranted = this.consent;\r\n this.consent = granted;\r\n if (granted && !wasGranted) {\r\n // flush 队列\r\n const queue = [...this.consentQueue];\r\n this.consentQueue.length = 0;\r\n for (const ev of queue) {\r\n this.dispatch([ev]).catch(() => {});\r\n }\r\n }\r\n }\r\n\r\n hasConsent(): boolean {\r\n return this.consent;\r\n }\r\n\r\n // ===== Storage =====\r\n\r\n setStorage(storage: IStorage): void {\r\n this.storageValue = storage;\r\n }\r\n\r\n getStorage(): IStorage | undefined {\r\n return this.storageValue;\r\n }\r\n\r\n // ===== Region Context(由平台层调用) =====\r\n\r\n /**\r\n * 平台层(如 @suucha/web 的 setupWeb)调用此方法注入 region 上下文。\r\n * 注入后会立即运行 router 函数确定 region。\r\n */\r\n setRegionContext(ctx: RegionContext): void {\r\n this.regionContext = ctx;\r\n this.resolveRegion();\r\n }\r\n\r\n getRegionContext(): RegionContext | null {\r\n return this.regionContext;\r\n }\r\n\r\n // ===== 广告 =====\r\n\r\n enableAds(manager: IAdManager): IAdManager {\r\n this.adManagerValue = manager;\r\n return manager;\r\n }\r\n\r\n getAdManager(): IAdManager | null {\r\n return this.adManagerValue;\r\n }\r\n\r\n // ===== 内部方法 =====\r\n\r\n private getActiveReporters(): ILogEventReporter[] {\r\n if (this.resolvedRegionReporters.length > 0) {\r\n return this.resolvedRegionReporters;\r\n }\r\n return [...this.reporters.values()];\r\n }\r\n\r\n private resolveRegion(): void {\r\n if (this.regionFn && this.regionContext) {\r\n const newRegion = this.regionFn(this.regionContext);\r\n if (newRegion !== this.currentRegion) {\r\n this.currentRegion = newRegion;\r\n }\r\n }\r\n\r\n const provider = this.regionReportersMap[this.currentRegion];\r\n if (Array.isArray(provider)) {\r\n this.resolvedRegionReporters = provider;\r\n } else if (typeof provider === 'function') {\r\n // 异步加载,先置空避免发送到错误 region\r\n this.resolvedRegionReporters = [];\r\n provider()\r\n .then((rs) => {\r\n this.resolvedRegionReporters = rs;\r\n })\r\n .catch((err) => {\r\n this.logger.log(\r\n LogLevel.Warning,\r\n `[Suucha] Failed to load reporters for region \"${this.currentRegion}\":`,\r\n err,\r\n );\r\n });\r\n }\r\n // 没有 provider 时保持现有 reporters(直接 addReporter 注册的)\r\n }\r\n\r\n private async dispatch(\r\n events: Array<{ name: string; params: Record<string, any> }>,\r\n ): Promise<void> {\r\n const reporters = this.getActiveReporters();\r\n if (reporters.length === 0) return;\r\n\r\n await Promise.all(\r\n events.flatMap((ev) =>\r\n reporters.map((r) =>\r\n r.logEvent(ev.name, ev.params).catch((err) => {\r\n this.logger.log(\r\n LogLevel.Error,\r\n `[Suucha] Reporter \"${r.name}\" failed to log \"${ev.name}\":`,\r\n err,\r\n );\r\n }),\r\n ),\r\n ),\r\n );\r\n }\r\n\r\n private generateAnonymousId(): string {\r\n const rand = Math.random().toString(36).slice(2, 10);\r\n return `anon_${Date.now().toString(36)}_${rand}`;\r\n }\r\n}\r\n\r\n/**\r\n * Suucha 全局单例\r\n */\r\nexport const suucha = new Suucha();\r\n"]}
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @suucha/core - Type Definitions
3
+ *
4
+ * 平台无关的核心接口定义。
5
+ * 此文件不依赖任何浏览器或 Node.js API,可被任何平台(Web、RN、小程序)复用。
6
+ */
7
+ declare enum LogLevel {
8
+ Trace = 0,
9
+ Debug = 1,
10
+ Information = 2,
11
+ Warning = 3,
12
+ Error = 4,
13
+ Critical = 5,
14
+ None = 6
15
+ }
16
+ interface LogEvent {
17
+ name: string;
18
+ params: Record<string, any>;
19
+ timestamp: number;
20
+ }
21
+ /**
22
+ * 日志事件上报器接口(第三方实现)
23
+ *
24
+ * 每个第三方服务(GA4、神策、Mixpanel 等)实现此接口,
25
+ * 通过 `suucha.addReporter(...)` 或 `suucha.region.reporters(...)` 注册。
26
+ */
27
+ interface ILogEventReporter {
28
+ readonly name: string;
29
+ logEvent(name: string, params: Record<string, any>): Promise<void>;
30
+ }
31
+ /**
32
+ * 事件拦截器:可产生新事件
33
+ *
34
+ * 用例:事件计数(spin 第 10 次自动产生 spin_010 事件)
35
+ */
36
+ interface ILogEventIntercept {
37
+ readonly name: string;
38
+ /** 越小越先执行 */
39
+ readonly order: number;
40
+ execute(eventName: string, params: Record<string, any>): Promise<{
41
+ events?: Array<{
42
+ name: string;
43
+ params: Record<string, any>;
44
+ }>;
45
+ }>;
46
+ }
47
+ /**
48
+ * 事件参数拦截器:可修改事件参数
49
+ *
50
+ * 用例:附加归因、用户身份、运行时长
51
+ */
52
+ interface ILogEventParameterIntercept {
53
+ readonly name: string;
54
+ /** 越小越先执行 */
55
+ readonly order: number;
56
+ execute(eventName: string, params: Record<string, any>): Promise<Record<string, any>>;
57
+ }
58
+ interface ILogger {
59
+ log(level: LogLevel, message: string, ...args: any[]): void;
60
+ }
61
+ interface IStorage {
62
+ get<T = any>(key: string): Promise<T | null>;
63
+ set<T = any>(key: string, value: T, ttl?: number): Promise<void>;
64
+ remove(key: string): Promise<void>;
65
+ }
66
+ interface AdSlotOptions {
67
+ container: string | HTMLElement;
68
+ slot?: string;
69
+ format?: 'auto' | 'horizontal' | 'vertical' | 'rectangle';
70
+ responsive?: boolean;
71
+ }
72
+ interface AdSlotHandle {
73
+ readonly id: string;
74
+ readonly slot: string;
75
+ destroy(): void;
76
+ }
77
+ interface IAdManager {
78
+ mountSlot(options: AdSlotOptions): Promise<AdSlotHandle>;
79
+ unmountSlot(handle: AdSlotHandle): void;
80
+ refresh?(handle: AdSlotHandle): void;
81
+ }
82
+ interface IConsentManager {
83
+ hasConsent(): boolean;
84
+ onConsentChange(cb: (granted: boolean) => void): () => void;
85
+ }
86
+ interface RegionContext {
87
+ host: string;
88
+ pathname: string;
89
+ timezone: string;
90
+ language: string;
91
+ url: string;
92
+ }
93
+ type ReporterProvider = ILogEventReporter[] | (() => Promise<ILogEventReporter[]>);
94
+ interface RegionRouter {
95
+ router(fn: (ctx: RegionContext) => string): void;
96
+ reporters(map: Record<string, ReporterProvider>): void;
97
+ getRegion(): string;
98
+ getReporters(): ILogEventReporter[];
99
+ }
100
+
101
+ /**
102
+ * @suucha/core - 内置拦截器
103
+ */
104
+
105
+ /**
106
+ * 事件计数配置
107
+ */
108
+ interface EventCounterConfiguration {
109
+ /** 事件名或前缀 */
110
+ name: string;
111
+ /** 触发的次数列表 */
112
+ counts: number[];
113
+ /** 名称补 0 长度,默认 3 */
114
+ padLength?: number;
115
+ }
116
+ /**
117
+ * 用户身份拦截器(自动启用)
118
+ * 给每个事件附加 anonymous_id 和(如果已 identify)user_id
119
+ */
120
+ declare class IdentityIntercept implements ILogEventParameterIntercept {
121
+ private readonly getAnonymousId;
122
+ private readonly getUserId;
123
+ readonly name = "suucha-identity";
124
+ readonly order = 100;
125
+ constructor(getAnonymousId: () => string, getUserId: () => string | null);
126
+ execute(_eventName: string, params: Record<string, any>): Promise<Record<string, any>>;
127
+ }
128
+ /**
129
+ * 事件计数拦截器
130
+ *
131
+ * 在配置的事件达到指定次数时,产生新事件(名称格式 {eventName}_{paddedCount})
132
+ */
133
+ declare class EventCounterIntercept implements ILogEventIntercept {
134
+ private readonly configurations;
135
+ readonly name = "suucha-event-counter";
136
+ readonly order = 200;
137
+ private readonly counts;
138
+ constructor(configurations: EventCounterConfiguration[]);
139
+ execute(eventName: string, params: Record<string, any>): Promise<{
140
+ events: {
141
+ name: string;
142
+ params: Record<string, any>;
143
+ }[];
144
+ }>;
145
+ }
146
+
147
+ /**
148
+ * @suucha/core - Suucha 主类与单例
149
+ */
150
+
151
+ declare class Suucha {
152
+ private readonly reporters;
153
+ private readonly eventIntercepts;
154
+ private readonly paramIntercepts;
155
+ private logger;
156
+ private logLevel;
157
+ private anonymousId;
158
+ private userId;
159
+ private consent;
160
+ private readonly consentQueue;
161
+ private regionFn?;
162
+ private regionReportersMap;
163
+ private currentRegion;
164
+ private resolvedRegionReporters;
165
+ private regionContext;
166
+ private adManagerValue;
167
+ private storageValue?;
168
+ readonly region: RegionRouter;
169
+ constructor();
170
+ setLogLevel(level: LogLevel): void;
171
+ getLogLevel(): LogLevel;
172
+ setLogger(logger: ILogger): void;
173
+ getLogger(): ILogger;
174
+ log(eventName: string, params?: Record<string, any>): Promise<void>;
175
+ addReporter(reporter: ILogEventReporter): void;
176
+ removeReporter(name: string): void;
177
+ getReporters(): ILogEventReporter[];
178
+ addEventIntercept(ic: ILogEventIntercept): void;
179
+ addParameterIntercept(ic: ILogEventParameterIntercept): void;
180
+ removeIntercept(name: string): void;
181
+ identify(userId: string, traits?: Record<string, any>): void;
182
+ getAnonymousId(): string;
183
+ setAnonymousId(id: string): void;
184
+ getUserId(): string | null;
185
+ reset(): void;
186
+ setConsent(granted: boolean): void;
187
+ hasConsent(): boolean;
188
+ setStorage(storage: IStorage): void;
189
+ getStorage(): IStorage | undefined;
190
+ /**
191
+ * 平台层(如 @suucha/web 的 setupWeb)调用此方法注入 region 上下文。
192
+ * 注入后会立即运行 router 函数确定 region。
193
+ */
194
+ setRegionContext(ctx: RegionContext): void;
195
+ getRegionContext(): RegionContext | null;
196
+ enableAds(manager: IAdManager): IAdManager;
197
+ getAdManager(): IAdManager | null;
198
+ private getActiveReporters;
199
+ private resolveRegion;
200
+ private dispatch;
201
+ private generateAnonymousId;
202
+ }
203
+ /**
204
+ * Suucha 全局单例
205
+ */
206
+ declare const suucha: Suucha;
207
+
208
+ export { type AdSlotHandle, type AdSlotOptions, type EventCounterConfiguration, EventCounterIntercept, type IAdManager, type IConsentManager, type ILogEventIntercept, type ILogEventParameterIntercept, type ILogEventReporter, type ILogger, type IStorage, IdentityIntercept, type LogEvent, LogLevel, type RegionContext, type RegionRouter, type ReporterProvider, Suucha, suucha };
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @suucha/core - Type Definitions
3
+ *
4
+ * 平台无关的核心接口定义。
5
+ * 此文件不依赖任何浏览器或 Node.js API,可被任何平台(Web、RN、小程序)复用。
6
+ */
7
+ declare enum LogLevel {
8
+ Trace = 0,
9
+ Debug = 1,
10
+ Information = 2,
11
+ Warning = 3,
12
+ Error = 4,
13
+ Critical = 5,
14
+ None = 6
15
+ }
16
+ interface LogEvent {
17
+ name: string;
18
+ params: Record<string, any>;
19
+ timestamp: number;
20
+ }
21
+ /**
22
+ * 日志事件上报器接口(第三方实现)
23
+ *
24
+ * 每个第三方服务(GA4、神策、Mixpanel 等)实现此接口,
25
+ * 通过 `suucha.addReporter(...)` 或 `suucha.region.reporters(...)` 注册。
26
+ */
27
+ interface ILogEventReporter {
28
+ readonly name: string;
29
+ logEvent(name: string, params: Record<string, any>): Promise<void>;
30
+ }
31
+ /**
32
+ * 事件拦截器:可产生新事件
33
+ *
34
+ * 用例:事件计数(spin 第 10 次自动产生 spin_010 事件)
35
+ */
36
+ interface ILogEventIntercept {
37
+ readonly name: string;
38
+ /** 越小越先执行 */
39
+ readonly order: number;
40
+ execute(eventName: string, params: Record<string, any>): Promise<{
41
+ events?: Array<{
42
+ name: string;
43
+ params: Record<string, any>;
44
+ }>;
45
+ }>;
46
+ }
47
+ /**
48
+ * 事件参数拦截器:可修改事件参数
49
+ *
50
+ * 用例:附加归因、用户身份、运行时长
51
+ */
52
+ interface ILogEventParameterIntercept {
53
+ readonly name: string;
54
+ /** 越小越先执行 */
55
+ readonly order: number;
56
+ execute(eventName: string, params: Record<string, any>): Promise<Record<string, any>>;
57
+ }
58
+ interface ILogger {
59
+ log(level: LogLevel, message: string, ...args: any[]): void;
60
+ }
61
+ interface IStorage {
62
+ get<T = any>(key: string): Promise<T | null>;
63
+ set<T = any>(key: string, value: T, ttl?: number): Promise<void>;
64
+ remove(key: string): Promise<void>;
65
+ }
66
+ interface AdSlotOptions {
67
+ container: string | HTMLElement;
68
+ slot?: string;
69
+ format?: 'auto' | 'horizontal' | 'vertical' | 'rectangle';
70
+ responsive?: boolean;
71
+ }
72
+ interface AdSlotHandle {
73
+ readonly id: string;
74
+ readonly slot: string;
75
+ destroy(): void;
76
+ }
77
+ interface IAdManager {
78
+ mountSlot(options: AdSlotOptions): Promise<AdSlotHandle>;
79
+ unmountSlot(handle: AdSlotHandle): void;
80
+ refresh?(handle: AdSlotHandle): void;
81
+ }
82
+ interface IConsentManager {
83
+ hasConsent(): boolean;
84
+ onConsentChange(cb: (granted: boolean) => void): () => void;
85
+ }
86
+ interface RegionContext {
87
+ host: string;
88
+ pathname: string;
89
+ timezone: string;
90
+ language: string;
91
+ url: string;
92
+ }
93
+ type ReporterProvider = ILogEventReporter[] | (() => Promise<ILogEventReporter[]>);
94
+ interface RegionRouter {
95
+ router(fn: (ctx: RegionContext) => string): void;
96
+ reporters(map: Record<string, ReporterProvider>): void;
97
+ getRegion(): string;
98
+ getReporters(): ILogEventReporter[];
99
+ }
100
+
101
+ /**
102
+ * @suucha/core - 内置拦截器
103
+ */
104
+
105
+ /**
106
+ * 事件计数配置
107
+ */
108
+ interface EventCounterConfiguration {
109
+ /** 事件名或前缀 */
110
+ name: string;
111
+ /** 触发的次数列表 */
112
+ counts: number[];
113
+ /** 名称补 0 长度,默认 3 */
114
+ padLength?: number;
115
+ }
116
+ /**
117
+ * 用户身份拦截器(自动启用)
118
+ * 给每个事件附加 anonymous_id 和(如果已 identify)user_id
119
+ */
120
+ declare class IdentityIntercept implements ILogEventParameterIntercept {
121
+ private readonly getAnonymousId;
122
+ private readonly getUserId;
123
+ readonly name = "suucha-identity";
124
+ readonly order = 100;
125
+ constructor(getAnonymousId: () => string, getUserId: () => string | null);
126
+ execute(_eventName: string, params: Record<string, any>): Promise<Record<string, any>>;
127
+ }
128
+ /**
129
+ * 事件计数拦截器
130
+ *
131
+ * 在配置的事件达到指定次数时,产生新事件(名称格式 {eventName}_{paddedCount})
132
+ */
133
+ declare class EventCounterIntercept implements ILogEventIntercept {
134
+ private readonly configurations;
135
+ readonly name = "suucha-event-counter";
136
+ readonly order = 200;
137
+ private readonly counts;
138
+ constructor(configurations: EventCounterConfiguration[]);
139
+ execute(eventName: string, params: Record<string, any>): Promise<{
140
+ events: {
141
+ name: string;
142
+ params: Record<string, any>;
143
+ }[];
144
+ }>;
145
+ }
146
+
147
+ /**
148
+ * @suucha/core - Suucha 主类与单例
149
+ */
150
+
151
+ declare class Suucha {
152
+ private readonly reporters;
153
+ private readonly eventIntercepts;
154
+ private readonly paramIntercepts;
155
+ private logger;
156
+ private logLevel;
157
+ private anonymousId;
158
+ private userId;
159
+ private consent;
160
+ private readonly consentQueue;
161
+ private regionFn?;
162
+ private regionReportersMap;
163
+ private currentRegion;
164
+ private resolvedRegionReporters;
165
+ private regionContext;
166
+ private adManagerValue;
167
+ private storageValue?;
168
+ readonly region: RegionRouter;
169
+ constructor();
170
+ setLogLevel(level: LogLevel): void;
171
+ getLogLevel(): LogLevel;
172
+ setLogger(logger: ILogger): void;
173
+ getLogger(): ILogger;
174
+ log(eventName: string, params?: Record<string, any>): Promise<void>;
175
+ addReporter(reporter: ILogEventReporter): void;
176
+ removeReporter(name: string): void;
177
+ getReporters(): ILogEventReporter[];
178
+ addEventIntercept(ic: ILogEventIntercept): void;
179
+ addParameterIntercept(ic: ILogEventParameterIntercept): void;
180
+ removeIntercept(name: string): void;
181
+ identify(userId: string, traits?: Record<string, any>): void;
182
+ getAnonymousId(): string;
183
+ setAnonymousId(id: string): void;
184
+ getUserId(): string | null;
185
+ reset(): void;
186
+ setConsent(granted: boolean): void;
187
+ hasConsent(): boolean;
188
+ setStorage(storage: IStorage): void;
189
+ getStorage(): IStorage | undefined;
190
+ /**
191
+ * 平台层(如 @suucha/web 的 setupWeb)调用此方法注入 region 上下文。
192
+ * 注入后会立即运行 router 函数确定 region。
193
+ */
194
+ setRegionContext(ctx: RegionContext): void;
195
+ getRegionContext(): RegionContext | null;
196
+ enableAds(manager: IAdManager): IAdManager;
197
+ getAdManager(): IAdManager | null;
198
+ private getActiveReporters;
199
+ private resolveRegion;
200
+ private dispatch;
201
+ private generateAnonymousId;
202
+ }
203
+ /**
204
+ * Suucha 全局单例
205
+ */
206
+ declare const suucha: Suucha;
207
+
208
+ export { type AdSlotHandle, type AdSlotOptions, type EventCounterConfiguration, EventCounterIntercept, type IAdManager, type IConsentManager, type ILogEventIntercept, type ILogEventParameterIntercept, type ILogEventReporter, type ILogger, type IStorage, IdentityIntercept, type LogEvent, LogLevel, type RegionContext, type RegionRouter, type ReporterProvider, Suucha, suucha };
package/dist/index.js ADDED
@@ -0,0 +1,286 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/types.ts
6
+ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
7
+ LogLevel2[LogLevel2["Trace"] = 0] = "Trace";
8
+ LogLevel2[LogLevel2["Debug"] = 1] = "Debug";
9
+ LogLevel2[LogLevel2["Information"] = 2] = "Information";
10
+ LogLevel2[LogLevel2["Warning"] = 3] = "Warning";
11
+ LogLevel2[LogLevel2["Error"] = 4] = "Error";
12
+ LogLevel2[LogLevel2["Critical"] = 5] = "Critical";
13
+ LogLevel2[LogLevel2["None"] = 6] = "None";
14
+ return LogLevel2;
15
+ })(LogLevel || {});
16
+
17
+ // src/interceptors.ts
18
+ var IdentityIntercept = class {
19
+ constructor(getAnonymousId, getUserId) {
20
+ __publicField(this, "getAnonymousId", getAnonymousId);
21
+ __publicField(this, "getUserId", getUserId);
22
+ __publicField(this, "name", "suucha-identity");
23
+ __publicField(this, "order", 100);
24
+ }
25
+ async execute(_eventName, params) {
26
+ const enriched = {
27
+ ...params,
28
+ anonymous_id: this.getAnonymousId()
29
+ };
30
+ const userId = this.getUserId();
31
+ if (userId) {
32
+ enriched.user_id = userId;
33
+ }
34
+ return enriched;
35
+ }
36
+ };
37
+ var EventCounterIntercept = class {
38
+ constructor(configurations) {
39
+ __publicField(this, "configurations", configurations);
40
+ __publicField(this, "name", "suucha-event-counter");
41
+ __publicField(this, "order", 200);
42
+ __publicField(this, "counts", /* @__PURE__ */ new Map());
43
+ }
44
+ async execute(eventName, params) {
45
+ const events = [];
46
+ for (const config of this.configurations) {
47
+ const isMatch = eventName === config.name || eventName.startsWith(`${config.name}_`);
48
+ if (!isMatch) continue;
49
+ const current = (this.counts.get(config.name) ?? 0) + 1;
50
+ this.counts.set(config.name, current);
51
+ if (config.counts.includes(current)) {
52
+ const padLength = config.padLength ?? 3;
53
+ const padded = String(current).padStart(padLength, "0");
54
+ events.push({
55
+ name: `${config.name}_${padded}`,
56
+ params: { ...params }
57
+ });
58
+ }
59
+ }
60
+ return { events };
61
+ }
62
+ };
63
+
64
+ // src/suucha.ts
65
+ var DefaultLogger = class {
66
+ log(_level, _message, ..._args) {
67
+ }
68
+ };
69
+ var DEFAULT_REGION = "default";
70
+ var Suucha = class {
71
+ constructor() {
72
+ __publicField(this, "reporters", /* @__PURE__ */ new Map());
73
+ __publicField(this, "eventIntercepts", []);
74
+ __publicField(this, "paramIntercepts", []);
75
+ __publicField(this, "logger", new DefaultLogger());
76
+ __publicField(this, "logLevel", 2 /* Information */);
77
+ __publicField(this, "anonymousId");
78
+ __publicField(this, "userId", null);
79
+ __publicField(this, "consent", true);
80
+ __publicField(this, "consentQueue", []);
81
+ __publicField(this, "regionFn");
82
+ __publicField(this, "regionReportersMap", {});
83
+ __publicField(this, "currentRegion", DEFAULT_REGION);
84
+ __publicField(this, "resolvedRegionReporters", []);
85
+ __publicField(this, "regionContext", null);
86
+ __publicField(this, "adManagerValue", null);
87
+ __publicField(this, "storageValue");
88
+ __publicField(this, "region");
89
+ this.anonymousId = this.generateAnonymousId();
90
+ this.paramIntercepts.push(
91
+ new IdentityIntercept(() => this.anonymousId, () => this.userId)
92
+ );
93
+ this.region = {
94
+ router: (fn) => {
95
+ this.regionFn = fn;
96
+ this.resolveRegion();
97
+ },
98
+ reporters: (map) => {
99
+ this.regionReportersMap = map;
100
+ this.resolveRegion();
101
+ },
102
+ getRegion: () => this.currentRegion,
103
+ getReporters: () => this.getActiveReporters()
104
+ };
105
+ }
106
+ // ===== 日志 =====
107
+ setLogLevel(level) {
108
+ this.logLevel = level;
109
+ }
110
+ getLogLevel() {
111
+ return this.logLevel;
112
+ }
113
+ setLogger(logger) {
114
+ this.logger = logger;
115
+ }
116
+ getLogger() {
117
+ return this.logger;
118
+ }
119
+ // ===== 埋点 =====
120
+ async log(eventName, params = {}) {
121
+ let enriched = { ...params };
122
+ for (const ic of this.paramIntercepts) {
123
+ enriched = await ic.execute(eventName, enriched);
124
+ }
125
+ const allEvents = [
126
+ { name: eventName, params: enriched }
127
+ ];
128
+ for (const ic of this.eventIntercepts) {
129
+ const result = await ic.execute(eventName, enriched);
130
+ if (result.events) {
131
+ allEvents.push(...result.events);
132
+ }
133
+ }
134
+ if (!this.consent) {
135
+ for (const ev of allEvents) {
136
+ this.consentQueue.push(ev);
137
+ }
138
+ return;
139
+ }
140
+ await this.dispatch(allEvents);
141
+ }
142
+ addReporter(reporter) {
143
+ this.reporters.set(reporter.name, reporter);
144
+ }
145
+ removeReporter(name) {
146
+ this.reporters.delete(name);
147
+ }
148
+ getReporters() {
149
+ return this.getActiveReporters();
150
+ }
151
+ // ===== 拦截器 =====
152
+ addEventIntercept(ic) {
153
+ this.eventIntercepts.push(ic);
154
+ this.eventIntercepts.sort((a, b) => a.order - b.order);
155
+ }
156
+ addParameterIntercept(ic) {
157
+ this.paramIntercepts.push(ic);
158
+ this.paramIntercepts.sort((a, b) => a.order - b.order);
159
+ }
160
+ removeIntercept(name) {
161
+ const ei = this.eventIntercepts.findIndex((x) => x.name === name);
162
+ if (ei >= 0) this.eventIntercepts.splice(ei, 1);
163
+ const pi = this.paramIntercepts.findIndex((x) => x.name === name);
164
+ if (pi >= 0) this.paramIntercepts.splice(pi, 1);
165
+ }
166
+ // ===== 用户身份 =====
167
+ identify(userId, traits) {
168
+ this.userId = userId;
169
+ if (traits) {
170
+ this.log("user_identify", { user_id: userId, ...traits }).catch(() => {
171
+ });
172
+ }
173
+ }
174
+ getAnonymousId() {
175
+ return this.anonymousId;
176
+ }
177
+ setAnonymousId(id) {
178
+ this.anonymousId = id;
179
+ }
180
+ getUserId() {
181
+ return this.userId;
182
+ }
183
+ reset() {
184
+ this.userId = null;
185
+ this.anonymousId = this.generateAnonymousId();
186
+ }
187
+ // ===== Consent =====
188
+ setConsent(granted) {
189
+ const wasGranted = this.consent;
190
+ this.consent = granted;
191
+ if (granted && !wasGranted) {
192
+ const queue = [...this.consentQueue];
193
+ this.consentQueue.length = 0;
194
+ for (const ev of queue) {
195
+ this.dispatch([ev]).catch(() => {
196
+ });
197
+ }
198
+ }
199
+ }
200
+ hasConsent() {
201
+ return this.consent;
202
+ }
203
+ // ===== Storage =====
204
+ setStorage(storage) {
205
+ this.storageValue = storage;
206
+ }
207
+ getStorage() {
208
+ return this.storageValue;
209
+ }
210
+ // ===== Region Context(由平台层调用) =====
211
+ /**
212
+ * 平台层(如 @suucha/web 的 setupWeb)调用此方法注入 region 上下文。
213
+ * 注入后会立即运行 router 函数确定 region。
214
+ */
215
+ setRegionContext(ctx) {
216
+ this.regionContext = ctx;
217
+ this.resolveRegion();
218
+ }
219
+ getRegionContext() {
220
+ return this.regionContext;
221
+ }
222
+ // ===== 广告 =====
223
+ enableAds(manager) {
224
+ this.adManagerValue = manager;
225
+ return manager;
226
+ }
227
+ getAdManager() {
228
+ return this.adManagerValue;
229
+ }
230
+ // ===== 内部方法 =====
231
+ getActiveReporters() {
232
+ if (this.resolvedRegionReporters.length > 0) {
233
+ return this.resolvedRegionReporters;
234
+ }
235
+ return [...this.reporters.values()];
236
+ }
237
+ resolveRegion() {
238
+ if (this.regionFn && this.regionContext) {
239
+ const newRegion = this.regionFn(this.regionContext);
240
+ if (newRegion !== this.currentRegion) {
241
+ this.currentRegion = newRegion;
242
+ }
243
+ }
244
+ const provider = this.regionReportersMap[this.currentRegion];
245
+ if (Array.isArray(provider)) {
246
+ this.resolvedRegionReporters = provider;
247
+ } else if (typeof provider === "function") {
248
+ this.resolvedRegionReporters = [];
249
+ provider().then((rs) => {
250
+ this.resolvedRegionReporters = rs;
251
+ }).catch((err) => {
252
+ this.logger.log(
253
+ 3 /* Warning */,
254
+ `[Suucha] Failed to load reporters for region "${this.currentRegion}":`,
255
+ err
256
+ );
257
+ });
258
+ }
259
+ }
260
+ async dispatch(events) {
261
+ const reporters = this.getActiveReporters();
262
+ if (reporters.length === 0) return;
263
+ await Promise.all(
264
+ events.flatMap(
265
+ (ev) => reporters.map(
266
+ (r) => r.logEvent(ev.name, ev.params).catch((err) => {
267
+ this.logger.log(
268
+ 4 /* Error */,
269
+ `[Suucha] Reporter "${r.name}" failed to log "${ev.name}":`,
270
+ err
271
+ );
272
+ })
273
+ )
274
+ )
275
+ );
276
+ }
277
+ generateAnonymousId() {
278
+ const rand = Math.random().toString(36).slice(2, 10);
279
+ return `anon_${Date.now().toString(36)}_${rand}`;
280
+ }
281
+ };
282
+ var suucha = new Suucha();
283
+
284
+ export { EventCounterIntercept, IdentityIntercept, LogLevel, Suucha, suucha };
285
+ //# sourceMappingURL=index.js.map
286
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/interceptors.ts","../src/suucha.ts"],"names":["LogLevel"],"mappings":";;;;;AASO,IAAK,QAAA,qBAAAA,SAAAA,KAAL;AACL,EAAAA,SAAAA,CAAAA,SAAAA,CAAA,WAAQ,CAAA,CAAA,GAAR,OAAA;AACA,EAAAA,SAAAA,CAAAA,SAAAA,CAAA,WAAQ,CAAA,CAAA,GAAR,OAAA;AACA,EAAAA,SAAAA,CAAAA,SAAAA,CAAA,iBAAc,CAAA,CAAA,GAAd,aAAA;AACA,EAAAA,SAAAA,CAAAA,SAAAA,CAAA,aAAU,CAAA,CAAA,GAAV,SAAA;AACA,EAAAA,SAAAA,CAAAA,SAAAA,CAAA,WAAQ,CAAA,CAAA,GAAR,OAAA;AACA,EAAAA,SAAAA,CAAAA,SAAAA,CAAA,cAAW,CAAA,CAAA,GAAX,UAAA;AACA,EAAAA,SAAAA,CAAAA,SAAAA,CAAA,UAAO,CAAA,CAAA,GAAP,MAAA;AAPU,EAAA,OAAAA,SAAAA;AAAA,CAAA,EAAA,QAAA,IAAA,EAAA;;;ACgBL,IAAM,oBAAN,MAA+D;AAAA,EAIpE,WAAA,CACmB,gBACA,SAAA,EACjB;AAFiB,IAAA,aAAA,CAAA,IAAA,EAAA,gBAAA,EAAA,cAAA,CAAA;AACA,IAAA,aAAA,CAAA,IAAA,EAAA,WAAA,EAAA,SAAA,CAAA;AALnB,IAAA,aAAA,CAAA,IAAA,EAAS,MAAA,EAAO,iBAAA,CAAA;AAChB,IAAA,aAAA,CAAA,IAAA,EAAS,OAAA,EAAQ,GAAA,CAAA;AAAA,EAKd;AAAA,EAEH,MAAM,OAAA,CACJ,UAAA,EACA,MAAA,EAC8B;AAC9B,IAAA,MAAM,QAAA,GAAgC;AAAA,MACpC,GAAG,MAAA;AAAA,MACH,YAAA,EAAc,KAAK,cAAA;AAAe,KACpC;AACA,IAAA,MAAM,MAAA,GAAS,KAAK,SAAA,EAAU;AAC9B,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,QAAA,CAAS,OAAA,GAAU,MAAA;AAAA,IACrB;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AACF;AAOO,IAAM,wBAAN,MAA0D;AAAA,EAM/D,YAA6B,cAAA,EAA6C;AAA7C,IAAA,aAAA,CAAA,IAAA,EAAA,gBAAA,EAAA,cAAA,CAAA;AAL7B,IAAA,aAAA,CAAA,IAAA,EAAS,MAAA,EAAO,sBAAA,CAAA;AAChB,IAAA,aAAA,CAAA,IAAA,EAAS,OAAA,EAAQ,GAAA,CAAA;AAEjB,IAAA,aAAA,CAAA,IAAA,EAAiB,QAAA,sBAAa,GAAA,EAAoB,CAAA;AAAA,EAEyB;AAAA,EAE3E,MAAM,OAAA,CAAQ,SAAA,EAAmB,MAAA,EAA6B;AAC5D,IAAA,MAAM,SAA+D,EAAC;AAEtE,IAAA,KAAA,MAAW,MAAA,IAAU,KAAK,cAAA,EAAgB;AACxC,MAAA,MAAM,OAAA,GACJ,cAAc,MAAA,CAAO,IAAA,IAAQ,UAAU,UAAA,CAAW,CAAA,EAAG,MAAA,CAAO,IAAI,CAAA,CAAA,CAAG,CAAA;AACrE,MAAA,IAAI,CAAC,OAAA,EAAS;AAEd,MAAA,MAAM,WAAW,IAAA,CAAK,MAAA,CAAO,IAAI,MAAA,CAAO,IAAI,KAAK,CAAA,IAAK,CAAA;AACtD,MAAA,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,MAAA,CAAO,IAAA,EAAM,OAAO,CAAA;AAEpC,MAAA,IAAI,MAAA,CAAO,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AACnC,QAAA,MAAM,SAAA,GAAY,OAAO,SAAA,IAAa,CAAA;AACtC,QAAA,MAAM,SAAS,MAAA,CAAO,OAAO,CAAA,CAAE,QAAA,CAAS,WAAW,GAAG,CAAA;AACtD,QAAA,MAAA,CAAO,IAAA,CAAK;AAAA,UACV,IAAA,EAAM,CAAA,EAAG,MAAA,CAAO,IAAI,IAAI,MAAM,CAAA,CAAA;AAAA,UAC9B,MAAA,EAAQ,EAAE,GAAG,MAAA;AAAO,SACrB,CAAA;AAAA,MACH;AAAA,IACF;AAEA,IAAA,OAAO,EAAE,MAAA,EAAO;AAAA,EAClB;AACF;;;AC/DA,IAAM,gBAAN,MAAuC;AAAA,EACrC,GAAA,CAAI,MAAA,EAAkB,QAAA,EAAA,GAAqB,KAAA,EAAoB;AAAA,EAE/D;AACF,CAAA;AAEA,IAAM,cAAA,GAAiB,SAAA;AAEhB,IAAM,SAAN,MAAa;AAAA,EAoBlB,WAAA,GAAc;AAnBd,IAAA,aAAA,CAAA,IAAA,EAAiB,WAAA,sBAAgB,GAAA,EAA+B,CAAA;AAChE,IAAA,aAAA,CAAA,IAAA,EAAiB,mBAAwC,EAAC,CAAA;AAC1D,IAAA,aAAA,CAAA,IAAA,EAAiB,mBAAiD,EAAC,CAAA;AACnE,IAAA,aAAA,CAAA,IAAA,EAAQ,QAAA,EAAkB,IAAI,aAAA,EAAc,CAAA;AAC5C,IAAA,aAAA,CAAA,IAAA,EAAQ,UAAA,EAAA,CAAA,mBAAA;AACR,IAAA,aAAA,CAAA,IAAA,EAAQ,aAAA,CAAA;AACR,IAAA,aAAA,CAAA,IAAA,EAAQ,QAAA,EAAwB,IAAA,CAAA;AAChC,IAAA,aAAA,CAAA,IAAA,EAAQ,SAAA,EAAU,IAAA,CAAA;AAClB,IAAA,aAAA,CAAA,IAAA,EAAiB,gBAAqE,EAAC,CAAA;AACvF,IAAA,aAAA,CAAA,IAAA,EAAQ,UAAA,CAAA;AACR,IAAA,aAAA,CAAA,IAAA,EAAQ,sBAAuD,EAAC,CAAA;AAChE,IAAA,aAAA,CAAA,IAAA,EAAQ,eAAA,EAAgB,cAAA,CAAA;AACxB,IAAA,aAAA,CAAA,IAAA,EAAQ,2BAA+C,EAAC,CAAA;AACxD,IAAA,aAAA,CAAA,IAAA,EAAQ,eAAA,EAAsC,IAAA,CAAA;AAC9C,IAAA,aAAA,CAAA,IAAA,EAAQ,gBAAA,EAAoC,IAAA,CAAA;AAC5C,IAAA,aAAA,CAAA,IAAA,EAAQ,cAAA,CAAA;AAER,IAAA,aAAA,CAAA,IAAA,EAAS,QAAA,CAAA;AAGP,IAAA,IAAA,CAAK,WAAA,GAAc,KAAK,mBAAA,EAAoB;AAC5C,IAAA,IAAA,CAAK,eAAA,CAAgB,IAAA;AAAA,MACnB,IAAI,iBAAA,CAAkB,MAAM,KAAK,WAAA,EAAa,MAAM,KAAK,MAAM;AAAA,KACjE;AAEA,IAAA,IAAA,CAAK,MAAA,GAAS;AAAA,MACZ,MAAA,EAAQ,CAAC,EAAA,KAAO;AACd,QAAA,IAAA,CAAK,QAAA,GAAW,EAAA;AAChB,QAAA,IAAA,CAAK,aAAA,EAAc;AAAA,MACrB,CAAA;AAAA,MACA,SAAA,EAAW,CAAC,GAAA,KAAQ;AAClB,QAAA,IAAA,CAAK,kBAAA,GAAqB,GAAA;AAC1B,QAAA,IAAA,CAAK,aAAA,EAAc;AAAA,MACrB,CAAA;AAAA,MACA,SAAA,EAAW,MAAM,IAAA,CAAK,aAAA;AAAA,MACtB,YAAA,EAAc,MAAM,IAAA,CAAK,kBAAA;AAAmB,KAC9C;AAAA,EACF;AAAA;AAAA,EAIA,YAAY,KAAA,EAAuB;AACjC,IAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAAA,EAClB;AAAA,EAEA,WAAA,GAAwB;AACtB,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AAAA,EAEA,UAAU,MAAA,EAAuB;AAC/B,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AAAA,EAEA,SAAA,GAAqB;AACnB,IAAA,OAAO,IAAA,CAAK,MAAA;AAAA,EACd;AAAA;AAAA,EAIA,MAAM,GAAA,CAAI,SAAA,EAAmB,MAAA,GAA8B,EAAC,EAAkB;AAE5E,IAAA,IAAI,QAAA,GAAW,EAAE,GAAG,MAAA,EAAO;AAC3B,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,eAAA,EAAiB;AACrC,MAAA,QAAA,GAAW,MAAM,EAAA,CAAG,OAAA,CAAQ,SAAA,EAAW,QAAQ,CAAA;AAAA,IACjD;AAGA,IAAA,MAAM,SAAA,GAAkE;AAAA,MACtE,EAAE,IAAA,EAAM,SAAA,EAAW,MAAA,EAAQ,QAAA;AAAS,KACtC;AACA,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,eAAA,EAAiB;AACrC,MAAA,MAAM,MAAA,GAAS,MAAM,EAAA,CAAG,OAAA,CAAQ,WAAW,QAAQ,CAAA;AACnD,MAAA,IAAI,OAAO,MAAA,EAAQ;AACjB,QAAA,SAAA,CAAU,IAAA,CAAK,GAAG,MAAA,CAAO,MAAM,CAAA;AAAA,MACjC;AAAA,IACF;AAGA,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AACjB,MAAA,KAAA,MAAW,MAAM,SAAA,EAAW;AAC1B,QAAA,IAAA,CAAK,YAAA,CAAa,KAAK,EAAE,CAAA;AAAA,MAC3B;AACA,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,IAAA,CAAK,SAAS,SAAS,CAAA;AAAA,EAC/B;AAAA,EAEA,YAAY,QAAA,EAAmC;AAC7C,IAAA,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,QAAA,CAAS,IAAA,EAAM,QAAQ,CAAA;AAAA,EAC5C;AAAA,EAEA,eAAe,IAAA,EAAoB;AACjC,IAAA,IAAA,CAAK,SAAA,CAAU,OAAO,IAAI,CAAA;AAAA,EAC5B;AAAA,EAEA,YAAA,GAAoC;AAClC,IAAA,OAAO,KAAK,kBAAA,EAAmB;AAAA,EACjC;AAAA;AAAA,EAIA,kBAAkB,EAAA,EAA8B;AAC9C,IAAA,IAAA,CAAK,eAAA,CAAgB,KAAK,EAAE,CAAA;AAC5B,IAAA,IAAA,CAAK,eAAA,CAAgB,KAAK,CAAC,CAAA,EAAG,MAAM,CAAA,CAAE,KAAA,GAAQ,EAAE,KAAK,CAAA;AAAA,EACvD;AAAA,EAEA,sBAAsB,EAAA,EAAuC;AAC3D,IAAA,IAAA,CAAK,eAAA,CAAgB,KAAK,EAAE,CAAA;AAC5B,IAAA,IAAA,CAAK,eAAA,CAAgB,KAAK,CAAC,CAAA,EAAG,MAAM,CAAA,CAAE,KAAA,GAAQ,EAAE,KAAK,CAAA;AAAA,EACvD;AAAA,EAEA,gBAAgB,IAAA,EAAoB;AAClC,IAAA,MAAM,EAAA,GAAK,KAAK,eAAA,CAAgB,SAAA,CAAU,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,IAAI,CAAA;AAChE,IAAA,IAAI,MAAM,CAAA,EAAG,IAAA,CAAK,eAAA,CAAgB,MAAA,CAAO,IAAI,CAAC,CAAA;AAC9C,IAAA,MAAM,EAAA,GAAK,KAAK,eAAA,CAAgB,SAAA,CAAU,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,IAAI,CAAA;AAChE,IAAA,IAAI,MAAM,CAAA,EAAG,IAAA,CAAK,eAAA,CAAgB,MAAA,CAAO,IAAI,CAAC,CAAA;AAAA,EAChD;AAAA;AAAA,EAIA,QAAA,CAAS,QAAgB,MAAA,EAAoC;AAC3D,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,IAAA,CAAK,GAAA,CAAI,eAAA,EAAiB,EAAE,OAAA,EAAS,MAAA,EAAQ,GAAG,MAAA,EAAQ,CAAA,CAAE,KAAA,CAAM,MAAM;AAAA,MAAC,CAAC,CAAA;AAAA,IAC1E;AAAA,EACF;AAAA,EAEA,cAAA,GAAyB;AACvB,IAAA,OAAO,IAAA,CAAK,WAAA;AAAA,EACd;AAAA,EAEA,eAAe,EAAA,EAAkB;AAC/B,IAAA,IAAA,CAAK,WAAA,GAAc,EAAA;AAAA,EACrB;AAAA,EAEA,SAAA,GAA2B;AACzB,IAAA,OAAO,IAAA,CAAK,MAAA;AAAA,EACd;AAAA,EAEA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AACd,IAAA,IAAA,CAAK,WAAA,GAAc,KAAK,mBAAA,EAAoB;AAAA,EAC9C;AAAA;AAAA,EAIA,WAAW,OAAA,EAAwB;AACjC,IAAA,MAAM,aAAa,IAAA,CAAK,OAAA;AACxB,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AACf,IAAA,IAAI,OAAA,IAAW,CAAC,UAAA,EAAY;AAE1B,MAAA,MAAM,KAAA,GAAQ,CAAC,GAAG,IAAA,CAAK,YAAY,CAAA;AACnC,MAAA,IAAA,CAAK,aAAa,MAAA,GAAS,CAAA;AAC3B,MAAA,KAAA,MAAW,MAAM,KAAA,EAAO;AACtB,QAAA,IAAA,CAAK,SAAS,CAAC,EAAE,CAAC,CAAA,CAAE,MAAM,MAAM;AAAA,QAAC,CAAC,CAAA;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAA,GAAsB;AACpB,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EACd;AAAA;AAAA,EAIA,WAAW,OAAA,EAAyB;AAClC,IAAA,IAAA,CAAK,YAAA,GAAe,OAAA;AAAA,EACtB;AAAA,EAEA,UAAA,GAAmC;AACjC,IAAA,OAAO,IAAA,CAAK,YAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,iBAAiB,GAAA,EAA0B;AACzC,IAAA,IAAA,CAAK,aAAA,GAAgB,GAAA;AACrB,IAAA,IAAA,CAAK,aAAA,EAAc;AAAA,EACrB;AAAA,EAEA,gBAAA,GAAyC;AACvC,IAAA,OAAO,IAAA,CAAK,aAAA;AAAA,EACd;AAAA;AAAA,EAIA,UAAU,OAAA,EAAiC;AACzC,IAAA,IAAA,CAAK,cAAA,GAAiB,OAAA;AACtB,IAAA,OAAO,OAAA;AAAA,EACT;AAAA,EAEA,YAAA,GAAkC;AAChC,IAAA,OAAO,IAAA,CAAK,cAAA;AAAA,EACd;AAAA;AAAA,EAIQ,kBAAA,GAA0C;AAChD,IAAA,IAAI,IAAA,CAAK,uBAAA,CAAwB,MAAA,GAAS,CAAA,EAAG;AAC3C,MAAA,OAAO,IAAA,CAAK,uBAAA;AAAA,IACd;AACA,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAA;AAAA,EACpC;AAAA,EAEQ,aAAA,GAAsB;AAC5B,IAAA,IAAI,IAAA,CAAK,QAAA,IAAY,IAAA,CAAK,aAAA,EAAe;AACvC,MAAA,MAAM,SAAA,GAAY,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,aAAa,CAAA;AAClD,MAAA,IAAI,SAAA,KAAc,KAAK,aAAA,EAAe;AACpC,QAAA,IAAA,CAAK,aAAA,GAAgB,SAAA;AAAA,MACvB;AAAA,IACF;AAEA,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,kBAAA,CAAmB,IAAA,CAAK,aAAa,CAAA;AAC3D,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,QAAQ,CAAA,EAAG;AAC3B,MAAA,IAAA,CAAK,uBAAA,GAA0B,QAAA;AAAA,IACjC,CAAA,MAAA,IAAW,OAAO,QAAA,KAAa,UAAA,EAAY;AAEzC,MAAA,IAAA,CAAK,0BAA0B,EAAC;AAChC,MAAA,QAAA,EAAS,CACN,IAAA,CAAK,CAAC,EAAA,KAAO;AACZ,QAAA,IAAA,CAAK,uBAAA,GAA0B,EAAA;AAAA,MACjC,CAAC,CAAA,CACA,KAAA,CAAM,CAAC,GAAA,KAAQ;AACd,QAAA,IAAA,CAAK,MAAA,CAAO,GAAA;AAAA,UAAA,CAAA;AAAA,UAEV,CAAA,8CAAA,EAAiD,KAAK,aAAa,CAAA,EAAA,CAAA;AAAA,UACnE;AAAA,SACF;AAAA,MACF,CAAC,CAAA;AAAA,IACL;AAAA,EAEF;AAAA,EAEA,MAAc,SACZ,MAAA,EACe;AACf,IAAA,MAAM,SAAA,GAAY,KAAK,kBAAA,EAAmB;AAC1C,IAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAE5B,IAAA,MAAM,OAAA,CAAQ,GAAA;AAAA,MACZ,MAAA,CAAO,OAAA;AAAA,QAAQ,CAAC,OACd,SAAA,CAAU,GAAA;AAAA,UAAI,CAAC,CAAA,KACb,CAAA,CAAE,QAAA,CAAS,EAAA,CAAG,IAAA,EAAM,EAAA,CAAG,MAAM,CAAA,CAAE,KAAA,CAAM,CAAC,GAAA,KAAQ;AAC5C,YAAA,IAAA,CAAK,MAAA,CAAO,GAAA;AAAA,cAAA,CAAA;AAAA,cAEV,CAAA,mBAAA,EAAsB,CAAA,CAAE,IAAI,CAAA,iBAAA,EAAoB,GAAG,IAAI,CAAA,EAAA,CAAA;AAAA,cACvD;AAAA,aACF;AAAA,UACF,CAAC;AAAA;AACH;AACF,KACF;AAAA,EACF;AAAA,EAEQ,mBAAA,GAA8B;AACpC,IAAA,MAAM,IAAA,GAAO,KAAK,MAAA,EAAO,CAAE,SAAS,EAAE,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AACnD,IAAA,OAAO,CAAA,KAAA,EAAQ,KAAK,GAAA,EAAI,CAAE,SAAS,EAAE,CAAC,IAAI,IAAI,CAAA,CAAA;AAAA,EAChD;AACF;AAKO,IAAM,MAAA,GAAS,IAAI,MAAA","file":"index.js","sourcesContent":["/**\r\n * @suucha/core - Type Definitions\r\n *\r\n * 平台无关的核心接口定义。\r\n * 此文件不依赖任何浏览器或 Node.js API,可被任何平台(Web、RN、小程序)复用。\r\n */\r\n\r\n// ===== 日志等级 =====\r\n\r\nexport enum LogLevel {\r\n Trace = 0,\r\n Debug = 1,\r\n Information = 2,\r\n Warning = 3,\r\n Error = 4,\r\n Critical = 5,\r\n None = 6,\r\n}\r\n\r\n// ===== 日志事件相关 =====\r\n\r\nexport interface LogEvent {\r\n name: string;\r\n params: Record<string, any>;\r\n timestamp: number;\r\n}\r\n\r\n/**\r\n * 日志事件上报器接口(第三方实现)\r\n *\r\n * 每个第三方服务(GA4、神策、Mixpanel 等)实现此接口,\r\n * 通过 `suucha.addReporter(...)` 或 `suucha.region.reporters(...)` 注册。\r\n */\r\nexport interface ILogEventReporter {\r\n readonly name: string;\r\n logEvent(name: string, params: Record<string, any>): Promise<void>;\r\n}\r\n\r\n/**\r\n * 事件拦截器:可产生新事件\r\n *\r\n * 用例:事件计数(spin 第 10 次自动产生 spin_010 事件)\r\n */\r\nexport interface ILogEventIntercept {\r\n readonly name: string;\r\n /** 越小越先执行 */\r\n readonly order: number;\r\n execute(\r\n eventName: string,\r\n params: Record<string, any>,\r\n ): Promise<{\r\n events?: Array<{ name: string; params: Record<string, any> }>;\r\n }>;\r\n}\r\n\r\n/**\r\n * 事件参数拦截器:可修改事件参数\r\n *\r\n * 用例:附加归因、用户身份、运行时长\r\n */\r\nexport interface ILogEventParameterIntercept {\r\n readonly name: string;\r\n /** 越小越先执行 */\r\n readonly order: number;\r\n execute(\r\n eventName: string,\r\n params: Record<string, any>,\r\n ): Promise<Record<string, any>>;\r\n}\r\n\r\n// ===== Logger =====\r\n\r\nexport interface ILogger {\r\n log(level: LogLevel, message: string, ...args: any[]): void;\r\n}\r\n\r\n// ===== Storage =====\r\n\r\nexport interface IStorage {\r\n get<T = any>(key: string): Promise<T | null>;\r\n set<T = any>(key: string, value: T, ttl?: number): Promise<void>;\r\n remove(key: string): Promise<void>;\r\n}\r\n\r\n// ===== 广告 =====\r\n\r\nexport interface AdSlotOptions {\r\n container: string | HTMLElement;\r\n slot?: string;\r\n format?: 'auto' | 'horizontal' | 'vertical' | 'rectangle';\r\n responsive?: boolean;\r\n}\r\n\r\nexport interface AdSlotHandle {\r\n readonly id: string;\r\n readonly slot: string;\r\n destroy(): void;\r\n}\r\n\r\nexport interface IAdManager {\r\n mountSlot(options: AdSlotOptions): Promise<AdSlotHandle>;\r\n unmountSlot(handle: AdSlotHandle): void;\r\n refresh?(handle: AdSlotHandle): void;\r\n}\r\n\r\n// ===== Consent =====\r\n\r\nexport interface IConsentManager {\r\n hasConsent(): boolean;\r\n onConsentChange(cb: (granted: boolean) => void): () => void;\r\n}\r\n\r\n// ===== Region 路由 =====\r\n\r\nexport interface RegionContext {\r\n host: string;\r\n pathname: string;\r\n timezone: string;\r\n language: string;\r\n url: string;\r\n}\r\n\r\nexport type ReporterProvider = ILogEventReporter[] | (() => Promise<ILogEventReporter[]>);\r\n\r\nexport interface RegionRouter {\r\n router(fn: (ctx: RegionContext) => string): void;\r\n reporters(map: Record<string, ReporterProvider>): void;\r\n getRegion(): string;\r\n getReporters(): ILogEventReporter[];\r\n}\r\n","/**\r\n * @suucha/core - 内置拦截器\r\n */\r\n\r\nimport type {\r\n ILogEventIntercept,\r\n ILogEventParameterIntercept,\r\n} from './types.js';\r\n\r\n/**\r\n * 事件计数配置\r\n */\r\nexport interface EventCounterConfiguration {\r\n /** 事件名或前缀 */\r\n name: string;\r\n /** 触发的次数列表 */\r\n counts: number[];\r\n /** 名称补 0 长度,默认 3 */\r\n padLength?: number;\r\n}\r\n\r\n/**\r\n * 用户身份拦截器(自动启用)\r\n * 给每个事件附加 anonymous_id 和(如果已 identify)user_id\r\n */\r\nexport class IdentityIntercept implements ILogEventParameterIntercept {\r\n readonly name = 'suucha-identity';\r\n readonly order = 100;\r\n\r\n constructor(\r\n private readonly getAnonymousId: () => string,\r\n private readonly getUserId: () => string | null,\r\n ) {}\r\n\r\n async execute(\r\n _eventName: string,\r\n params: Record<string, any>,\r\n ): Promise<Record<string, any>> {\r\n const enriched: Record<string, any> = {\r\n ...params,\r\n anonymous_id: this.getAnonymousId(),\r\n };\r\n const userId = this.getUserId();\r\n if (userId) {\r\n enriched.user_id = userId;\r\n }\r\n return enriched;\r\n }\r\n}\r\n\r\n/**\r\n * 事件计数拦截器\r\n *\r\n * 在配置的事件达到指定次数时,产生新事件(名称格式 {eventName}_{paddedCount})\r\n */\r\nexport class EventCounterIntercept implements ILogEventIntercept {\r\n readonly name = 'suucha-event-counter';\r\n readonly order = 200;\r\n\r\n private readonly counts = new Map<string, number>();\r\n\r\n constructor(private readonly configurations: EventCounterConfiguration[]) {}\r\n\r\n async execute(eventName: string, params: Record<string, any>) {\r\n const events: Array<{ name: string; params: Record<string, any> }> = [];\r\n\r\n for (const config of this.configurations) {\r\n const isMatch =\r\n eventName === config.name || eventName.startsWith(`${config.name}_`);\r\n if (!isMatch) continue;\r\n\r\n const current = (this.counts.get(config.name) ?? 0) + 1;\r\n this.counts.set(config.name, current);\r\n\r\n if (config.counts.includes(current)) {\r\n const padLength = config.padLength ?? 3;\r\n const padded = String(current).padStart(padLength, '0');\r\n events.push({\r\n name: `${config.name}_${padded}`,\r\n params: { ...params },\r\n });\r\n }\r\n }\r\n\r\n return { events };\r\n }\r\n}\r\n","/**\r\n * @suucha/core - Suucha 主类与单例\r\n */\r\n\r\nimport {\r\n IdentityIntercept,\r\n} from './interceptors.js';\r\nimport { LogLevel } from './types.js';\r\nimport type {\r\n ILogEventIntercept,\r\n ILogEventParameterIntercept,\r\n ILogEventReporter,\r\n ILogger,\r\n IStorage,\r\n IAdManager,\r\n RegionContext,\r\n RegionRouter,\r\n ReporterProvider,\r\n} from './types.js';\r\n\r\n/**\r\n * 默认 no-op logger,未配置 logger 时使用\r\n */\r\nclass DefaultLogger implements ILogger {\r\n log(_level: LogLevel, _message: string, ..._args: any[]): void {\r\n // no-op\r\n }\r\n}\r\n\r\nconst DEFAULT_REGION = 'default';\r\n\r\nexport class Suucha {\r\n private readonly reporters = new Map<string, ILogEventReporter>();\r\n private readonly eventIntercepts: ILogEventIntercept[] = [];\r\n private readonly paramIntercepts: ILogEventParameterIntercept[] = [];\r\n private logger: ILogger = new DefaultLogger();\r\n private logLevel: LogLevel = LogLevel.Information;\r\n private anonymousId: string;\r\n private userId: string | null = null;\r\n private consent = true;\r\n private readonly consentQueue: Array<{ name: string; params: Record<string, any> }> = [];\r\n private regionFn?: (ctx: RegionContext) => string;\r\n private regionReportersMap: Record<string, ReporterProvider> = {};\r\n private currentRegion = DEFAULT_REGION;\r\n private resolvedRegionReporters: ILogEventReporter[] = [];\r\n private regionContext: RegionContext | null = null;\r\n private adManagerValue: IAdManager | null = null;\r\n private storageValue?: IStorage;\r\n\r\n readonly region: RegionRouter;\r\n\r\n constructor() {\r\n this.anonymousId = this.generateAnonymousId();\r\n this.paramIntercepts.push(\r\n new IdentityIntercept(() => this.anonymousId, () => this.userId),\r\n );\r\n\r\n this.region = {\r\n router: (fn) => {\r\n this.regionFn = fn;\r\n this.resolveRegion();\r\n },\r\n reporters: (map) => {\r\n this.regionReportersMap = map;\r\n this.resolveRegion();\r\n },\r\n getRegion: () => this.currentRegion,\r\n getReporters: () => this.getActiveReporters(),\r\n };\r\n }\r\n\r\n // ===== 日志 =====\r\n\r\n setLogLevel(level: LogLevel): void {\r\n this.logLevel = level;\r\n }\r\n\r\n getLogLevel(): LogLevel {\r\n return this.logLevel;\r\n }\r\n\r\n setLogger(logger: ILogger): void {\r\n this.logger = logger;\r\n }\r\n\r\n getLogger(): ILogger {\r\n return this.logger;\r\n }\r\n\r\n // ===== 埋点 =====\r\n\r\n async log(eventName: string, params: Record<string, any> = {}): Promise<void> {\r\n // 1. 参数拦截器链\r\n let enriched = { ...params };\r\n for (const ic of this.paramIntercepts) {\r\n enriched = await ic.execute(eventName, enriched);\r\n }\r\n\r\n // 2. 事件拦截器链(可产生新事件)\r\n const allEvents: Array<{ name: string; params: Record<string, any> }> = [\r\n { name: eventName, params: enriched },\r\n ];\r\n for (const ic of this.eventIntercepts) {\r\n const result = await ic.execute(eventName, enriched);\r\n if (result.events) {\r\n allEvents.push(...result.events);\r\n }\r\n }\r\n\r\n // 3. Consent 检查:未同意则入队,不外发\r\n if (!this.consent) {\r\n for (const ev of allEvents) {\r\n this.consentQueue.push(ev);\r\n }\r\n return;\r\n }\r\n\r\n // 4. 分发到 reporters\r\n await this.dispatch(allEvents);\r\n }\r\n\r\n addReporter(reporter: ILogEventReporter): void {\r\n this.reporters.set(reporter.name, reporter);\r\n }\r\n\r\n removeReporter(name: string): void {\r\n this.reporters.delete(name);\r\n }\r\n\r\n getReporters(): ILogEventReporter[] {\r\n return this.getActiveReporters();\r\n }\r\n\r\n // ===== 拦截器 =====\r\n\r\n addEventIntercept(ic: ILogEventIntercept): void {\r\n this.eventIntercepts.push(ic);\r\n this.eventIntercepts.sort((a, b) => a.order - b.order);\r\n }\r\n\r\n addParameterIntercept(ic: ILogEventParameterIntercept): void {\r\n this.paramIntercepts.push(ic);\r\n this.paramIntercepts.sort((a, b) => a.order - b.order);\r\n }\r\n\r\n removeIntercept(name: string): void {\r\n const ei = this.eventIntercepts.findIndex((x) => x.name === name);\r\n if (ei >= 0) this.eventIntercepts.splice(ei, 1);\r\n const pi = this.paramIntercepts.findIndex((x) => x.name === name);\r\n if (pi >= 0) this.paramIntercepts.splice(pi, 1);\r\n }\r\n\r\n // ===== 用户身份 =====\r\n\r\n identify(userId: string, traits?: Record<string, any>): void {\r\n this.userId = userId;\r\n if (traits) {\r\n this.log('user_identify', { user_id: userId, ...traits }).catch(() => {});\r\n }\r\n }\r\n\r\n getAnonymousId(): string {\r\n return this.anonymousId;\r\n }\r\n\r\n setAnonymousId(id: string): void {\r\n this.anonymousId = id;\r\n }\r\n\r\n getUserId(): string | null {\r\n return this.userId;\r\n }\r\n\r\n reset(): void {\r\n this.userId = null;\r\n this.anonymousId = this.generateAnonymousId();\r\n }\r\n\r\n // ===== Consent =====\r\n\r\n setConsent(granted: boolean): void {\r\n const wasGranted = this.consent;\r\n this.consent = granted;\r\n if (granted && !wasGranted) {\r\n // flush 队列\r\n const queue = [...this.consentQueue];\r\n this.consentQueue.length = 0;\r\n for (const ev of queue) {\r\n this.dispatch([ev]).catch(() => {});\r\n }\r\n }\r\n }\r\n\r\n hasConsent(): boolean {\r\n return this.consent;\r\n }\r\n\r\n // ===== Storage =====\r\n\r\n setStorage(storage: IStorage): void {\r\n this.storageValue = storage;\r\n }\r\n\r\n getStorage(): IStorage | undefined {\r\n return this.storageValue;\r\n }\r\n\r\n // ===== Region Context(由平台层调用) =====\r\n\r\n /**\r\n * 平台层(如 @suucha/web 的 setupWeb)调用此方法注入 region 上下文。\r\n * 注入后会立即运行 router 函数确定 region。\r\n */\r\n setRegionContext(ctx: RegionContext): void {\r\n this.regionContext = ctx;\r\n this.resolveRegion();\r\n }\r\n\r\n getRegionContext(): RegionContext | null {\r\n return this.regionContext;\r\n }\r\n\r\n // ===== 广告 =====\r\n\r\n enableAds(manager: IAdManager): IAdManager {\r\n this.adManagerValue = manager;\r\n return manager;\r\n }\r\n\r\n getAdManager(): IAdManager | null {\r\n return this.adManagerValue;\r\n }\r\n\r\n // ===== 内部方法 =====\r\n\r\n private getActiveReporters(): ILogEventReporter[] {\r\n if (this.resolvedRegionReporters.length > 0) {\r\n return this.resolvedRegionReporters;\r\n }\r\n return [...this.reporters.values()];\r\n }\r\n\r\n private resolveRegion(): void {\r\n if (this.regionFn && this.regionContext) {\r\n const newRegion = this.regionFn(this.regionContext);\r\n if (newRegion !== this.currentRegion) {\r\n this.currentRegion = newRegion;\r\n }\r\n }\r\n\r\n const provider = this.regionReportersMap[this.currentRegion];\r\n if (Array.isArray(provider)) {\r\n this.resolvedRegionReporters = provider;\r\n } else if (typeof provider === 'function') {\r\n // 异步加载,先置空避免发送到错误 region\r\n this.resolvedRegionReporters = [];\r\n provider()\r\n .then((rs) => {\r\n this.resolvedRegionReporters = rs;\r\n })\r\n .catch((err) => {\r\n this.logger.log(\r\n LogLevel.Warning,\r\n `[Suucha] Failed to load reporters for region \"${this.currentRegion}\":`,\r\n err,\r\n );\r\n });\r\n }\r\n // 没有 provider 时保持现有 reporters(直接 addReporter 注册的)\r\n }\r\n\r\n private async dispatch(\r\n events: Array<{ name: string; params: Record<string, any> }>,\r\n ): Promise<void> {\r\n const reporters = this.getActiveReporters();\r\n if (reporters.length === 0) return;\r\n\r\n await Promise.all(\r\n events.flatMap((ev) =>\r\n reporters.map((r) =>\r\n r.logEvent(ev.name, ev.params).catch((err) => {\r\n this.logger.log(\r\n LogLevel.Error,\r\n `[Suucha] Reporter \"${r.name}\" failed to log \"${ev.name}\":`,\r\n err,\r\n );\r\n }),\r\n ),\r\n ),\r\n );\r\n }\r\n\r\n private generateAnonymousId(): string {\r\n const rand = Math.random().toString(36).slice(2, 10);\r\n return `anon_${Date.now().toString(36)}_${rand}`;\r\n }\r\n}\r\n\r\n/**\r\n * Suucha 全局单例\r\n */\r\nexport const suucha = new Suucha();\r\n"]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@suucha/core",
3
+ "version": "0.1.0",
4
+ "description": "Core abstractions, types, and the Suucha singleton for Suucha Web SDK",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "keywords": [
21
+ "suucha",
22
+ "analytics",
23
+ "sdk",
24
+ "core",
25
+ "typescript"
26
+ ],
27
+ "license": "MIT",
28
+ "author": "Suucha Studio",
29
+ "homepage": "https://github.com/suucha/Suucha.Web.Core#readme",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/suucha/Suucha.Web.Core.git",
33
+ "directory": "packages/core"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/suucha/Suucha.Web.Core/issues"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "dev": "tsup --watch",
44
+ "typecheck": "tsc --noEmit"
45
+ }
46
+ }