ptech-shell-dev 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/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # ptech-shell-dev
2
+
3
+ Standalone/mock implementations for services defined in `ptech-shell-sdk`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i ptech-shell-dev ptech-shell-sdk
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { initStandaloneServices } from 'ptech-shell-dev';
15
+
16
+ initStandaloneServices();
17
+ ```
@@ -0,0 +1,57 @@
1
+ import { Lang, User, I18nService, UserService, ApiClient, NavigationService, ConfigService, PermissionService, SharedStateService, ObservabilityService, NotificationService, AnalyticsService } from 'ptech-shell-sdk';
2
+
3
+ type StandaloneServices = {
4
+ i18n: I18nService;
5
+ userService: UserService;
6
+ apiClient: ApiClient;
7
+ navigation: NavigationService;
8
+ configService: ConfigService;
9
+ permissionService: PermissionService;
10
+ sharedState: SharedStateService;
11
+ observability: ObservabilityService;
12
+ notification: NotificationService;
13
+ analytics: AnalyticsService;
14
+ };
15
+ type ServiceRegisterMode = 'if-missing' | 'always';
16
+ type StandaloneInitOptions = {
17
+ /**
18
+ * API base URL when running standalone.
19
+ * Example: http://localhost:4000
20
+ */
21
+ apiBase?: string;
22
+ /**
23
+ * Default language for standalone.
24
+ */
25
+ lang?: Lang;
26
+ /**
27
+ * Seed user for standalone. (Optional)
28
+ */
29
+ user?: User | null;
30
+ /**
31
+ * Optional hook to customize the created services.
32
+ * This is useful when each remote wants slightly different mock behavior.
33
+ */
34
+ customize?: (services: StandaloneServices) => void;
35
+ /**
36
+ * Service registration strategy for standalone init.
37
+ * - if-missing (default): only register when token has no service yet.
38
+ * - always: replace any existing service for the token.
39
+ */
40
+ registerMode?: ServiceRegisterMode;
41
+ };
42
+
43
+ /**
44
+ * Initialize "safe defaults" so a remote app can run standalone without the host.
45
+ *
46
+ * In host-composed mode, host should register real services first.
47
+ * But if host doesn't (or remote is standalone), this provides mocks.
48
+ */
49
+ declare function initStandaloneServices(options?: StandaloneInitOptions): void;
50
+
51
+ declare function createStandaloneApiClient(apiBase: string): ApiClient;
52
+
53
+ declare function createStandaloneI18nService(initialLang: Lang): I18nService;
54
+
55
+ declare function createStandaloneUserService(user: User | null): UserService;
56
+
57
+ export { type ServiceRegisterMode, type StandaloneInitOptions, type StandaloneServices, createStandaloneApiClient, createStandaloneI18nService, createStandaloneUserService, initStandaloneServices };
package/dist/index.js ADDED
@@ -0,0 +1,492 @@
1
+ // src/initStandaloneServices.ts
2
+ import { FEATURE_FLAGS as FEATURE_FLAGS2, getService, registerService, SHARED_STATE_KEYS, TOKENS } from "ptech-shell-sdk";
3
+
4
+ // src/services/analytics.ts
5
+ var MAX_EVENTS = 200;
6
+ function createStandaloneAnalyticsService() {
7
+ const listeners = /* @__PURE__ */ new Set();
8
+ const events = [];
9
+ function emitChange() {
10
+ for (const listener of listeners) {
11
+ listener();
12
+ }
13
+ }
14
+ return {
15
+ track: (event) => {
16
+ events.push({
17
+ ...event,
18
+ timestamp: Date.now()
19
+ });
20
+ if (events.length > MAX_EVENTS) {
21
+ events.shift();
22
+ }
23
+ emitChange();
24
+ },
25
+ getEvents: () => [...events],
26
+ subscribe: (listener) => {
27
+ listeners.add(listener);
28
+ return () => listeners.delete(listener);
29
+ }
30
+ };
31
+ }
32
+
33
+ // src/services/apiClient.ts
34
+ function createStandaloneApiClient(apiBase) {
35
+ return {
36
+ fetch: async (path, init) => {
37
+ const url = path.startsWith("http") ? path : `${apiBase}${path.startsWith("/") ? "" : "/"}${path}`;
38
+ const headers = new Headers(init?.headers);
39
+ if (!headers.has("Authorization")) {
40
+ headers.set("Authorization", "Bearer dev-token");
41
+ }
42
+ return fetch(url, { ...init, headers });
43
+ }
44
+ };
45
+ }
46
+
47
+ // src/services/config.ts
48
+ import { FEATURE_FLAGS } from "ptech-shell-sdk";
49
+ var DEFAULT_FLAGS = {
50
+ [FEATURE_FLAGS.uiExperimental]: true
51
+ };
52
+ var DEFAULT_CONFIG = {
53
+ envName: "standalone",
54
+ apiBase: "http://localhost:4000"
55
+ };
56
+ function createStandaloneConfigService(initialFlags = DEFAULT_FLAGS, initialConfig = DEFAULT_CONFIG) {
57
+ const listeners = /* @__PURE__ */ new Set();
58
+ const flags = { ...initialFlags };
59
+ const runtimeConfig = { ...initialConfig };
60
+ function emitChange() {
61
+ for (const listener of listeners) {
62
+ listener();
63
+ }
64
+ }
65
+ return {
66
+ isEnabled: (flag) => Boolean(flags[flag]),
67
+ getAllFlags: () => ({ ...flags }),
68
+ setFlag: (flag, enabled) => {
69
+ if (flags[flag] === enabled) {
70
+ return;
71
+ }
72
+ flags[flag] = enabled;
73
+ emitChange();
74
+ },
75
+ getValue: (key) => runtimeConfig[key],
76
+ getAllConfig: () => ({ ...runtimeConfig }),
77
+ setValue: (key, value) => {
78
+ if (runtimeConfig[key] === value) {
79
+ return;
80
+ }
81
+ runtimeConfig[key] = value;
82
+ emitChange();
83
+ },
84
+ subscribe: (listener) => {
85
+ listeners.add(listener);
86
+ return () => listeners.delete(listener);
87
+ }
88
+ };
89
+ }
90
+
91
+ // src/services/i18n.ts
92
+ var BASE_NAMESPACE = "shell-dev";
93
+ var DEFAULT_MESSAGES = {
94
+ vi: {
95
+ "common.hello": "Xin chao",
96
+ "common.language": "Ngon ngu",
97
+ "common.user": "Nguoi dung",
98
+ "common.guest": "Khach",
99
+ "common.callApi": "Goi API"
100
+ },
101
+ en: {
102
+ "common.hello": "Hello",
103
+ "common.language": "Language",
104
+ "common.user": "User",
105
+ "common.guest": "Guest",
106
+ "common.callApi": "Call API"
107
+ }
108
+ };
109
+ function createStandaloneI18nService(initialLang) {
110
+ const listeners = /* @__PURE__ */ new Set();
111
+ let currentLang = initialLang;
112
+ const messagesByNamespace = /* @__PURE__ */ new Map();
113
+ messagesByNamespace.set(BASE_NAMESPACE, DEFAULT_MESSAGES);
114
+ function emitChange() {
115
+ for (const listener of listeners) {
116
+ listener();
117
+ }
118
+ }
119
+ function resolveMessage(key) {
120
+ for (const messages of messagesByNamespace.values()) {
121
+ const hit = messages[currentLang]?.[key];
122
+ if (hit !== void 0) {
123
+ return hit;
124
+ }
125
+ }
126
+ return key;
127
+ }
128
+ return {
129
+ t: (key) => resolveMessage(key),
130
+ getLang: () => currentLang,
131
+ setLang: (nextLang) => {
132
+ if (nextLang === currentLang) {
133
+ return;
134
+ }
135
+ currentLang = nextLang;
136
+ emitChange();
137
+ },
138
+ registerMessages: (namespace, messages) => {
139
+ messagesByNamespace.set(namespace, messages);
140
+ emitChange();
141
+ },
142
+ unregisterMessages: (namespace) => {
143
+ if (namespace === BASE_NAMESPACE) {
144
+ return;
145
+ }
146
+ const changed = messagesByNamespace.delete(namespace);
147
+ if (changed) {
148
+ emitChange();
149
+ }
150
+ },
151
+ subscribe: (listener) => {
152
+ listeners.add(listener);
153
+ return () => listeners.delete(listener);
154
+ }
155
+ };
156
+ }
157
+
158
+ // src/services/navigation.ts
159
+ function createStandaloneNavigationService(initialPath = "/standalone") {
160
+ const listeners = /* @__PURE__ */ new Set();
161
+ let currentPath = initialPath;
162
+ function emitChange() {
163
+ for (const listener of listeners) {
164
+ listener();
165
+ }
166
+ }
167
+ return {
168
+ getPath: () => currentPath,
169
+ navigate: (path) => {
170
+ if (path === currentPath) {
171
+ return;
172
+ }
173
+ currentPath = path;
174
+ emitChange();
175
+ },
176
+ subscribe: (listener) => {
177
+ listeners.add(listener);
178
+ return () => listeners.delete(listener);
179
+ }
180
+ };
181
+ }
182
+
183
+ // src/services/notification.ts
184
+ function createStandaloneNotificationService() {
185
+ const listeners = /* @__PURE__ */ new Set();
186
+ const items = [];
187
+ const timersById = /* @__PURE__ */ new Map();
188
+ function emitChange() {
189
+ for (const listener of listeners) {
190
+ listener();
191
+ }
192
+ }
193
+ function dismiss(id) {
194
+ const timer = timersById.get(id);
195
+ if (timer) {
196
+ clearTimeout(timer);
197
+ timersById.delete(id);
198
+ }
199
+ const index = items.findIndex((item) => item.id === id);
200
+ if (index < 0) {
201
+ return;
202
+ }
203
+ items.splice(index, 1);
204
+ emitChange();
205
+ }
206
+ return {
207
+ push: (input) => {
208
+ const id = `notif-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
209
+ const item = {
210
+ id,
211
+ level: input.level,
212
+ message: input.message,
213
+ source: input.source,
214
+ createdAt: Date.now()
215
+ };
216
+ items.unshift(item);
217
+ emitChange();
218
+ if (input.ttlMs && input.ttlMs > 0) {
219
+ const timer = setTimeout(() => dismiss(id), input.ttlMs);
220
+ timersById.set(id, timer);
221
+ }
222
+ return id;
223
+ },
224
+ dismiss,
225
+ getSnapshot: () => [...items],
226
+ subscribe: (listener) => {
227
+ listeners.add(listener);
228
+ return () => listeners.delete(listener);
229
+ }
230
+ };
231
+ }
232
+
233
+ // src/services/observability.ts
234
+ var MAX_ENTRIES = 100;
235
+ var LOG_METHOD = {
236
+ debug: "log",
237
+ info: "info",
238
+ warn: "warn",
239
+ error: "error"
240
+ };
241
+ function createStandaloneObservabilityService() {
242
+ const listeners = /* @__PURE__ */ new Set();
243
+ const entries = [];
244
+ let nextId = 1;
245
+ function emitChange() {
246
+ for (const listener of listeners) {
247
+ listener();
248
+ }
249
+ }
250
+ return {
251
+ log: (entry) => {
252
+ const nextEntry = {
253
+ ...entry,
254
+ id: nextId,
255
+ timestamp: Date.now()
256
+ };
257
+ nextId += 1;
258
+ entries.push(nextEntry);
259
+ if (entries.length > MAX_ENTRIES) {
260
+ entries.shift();
261
+ }
262
+ const method = LOG_METHOD[nextEntry.level];
263
+ console[method](`[${nextEntry.source}] ${nextEntry.message}`, nextEntry.data ?? "");
264
+ emitChange();
265
+ },
266
+ getEntries: () => [...entries],
267
+ subscribe: (listener) => {
268
+ listeners.add(listener);
269
+ return () => listeners.delete(listener);
270
+ }
271
+ };
272
+ }
273
+
274
+ // src/services/permission.ts
275
+ import {
276
+ listPermissionsByRole,
277
+ USER_ROLES
278
+ } from "ptech-shell-sdk";
279
+ var DEFAULT_PERMISSIONS = listPermissionsByRole(USER_ROLES.authenticated);
280
+ function createStandalonePermissionService(initialPermissions = DEFAULT_PERMISSIONS) {
281
+ const listeners = /* @__PURE__ */ new Set();
282
+ const permissions = new Set(initialPermissions);
283
+ return {
284
+ can: (permission) => permissions.has(permission),
285
+ list: () => Array.from(permissions.values()),
286
+ subscribe: (listener) => {
287
+ listeners.add(listener);
288
+ return () => listeners.delete(listener);
289
+ }
290
+ };
291
+ }
292
+
293
+ // src/services/sharedState.ts
294
+ function createStandaloneSharedStateService() {
295
+ const records = /* @__PURE__ */ new Map();
296
+ const listenersByKey = /* @__PURE__ */ new Map();
297
+ const requestListenersByKey = /* @__PURE__ */ new Map();
298
+ function emitKeyChanged(key) {
299
+ const listeners = listenersByKey.get(key);
300
+ if (!listeners) {
301
+ return;
302
+ }
303
+ for (const listener of listeners) {
304
+ listener();
305
+ }
306
+ }
307
+ function applyOwnerUpdate(key, value, owner) {
308
+ const current = records.get(key);
309
+ if (!current) {
310
+ throw new Error(`sharedState key "${key}" is not registered.`);
311
+ }
312
+ if (current.owner !== owner) {
313
+ throw new Error(`Only owner "${current.owner}" can set key "${key}".`);
314
+ }
315
+ records.set(key, {
316
+ ...current,
317
+ value,
318
+ version: current.version + 1
319
+ });
320
+ emitKeyChanged(key);
321
+ }
322
+ return {
323
+ ensureKey: (key, owner, initialValue) => {
324
+ if (records.has(key)) {
325
+ return;
326
+ }
327
+ records.set(key, {
328
+ key,
329
+ owner,
330
+ value: initialValue,
331
+ version: 1
332
+ });
333
+ emitKeyChanged(key);
334
+ },
335
+ getSnapshot: (key) => {
336
+ const record = records.get(key);
337
+ return record ? { ...record } : void 0;
338
+ },
339
+ getAll: () => Array.from(records.values()).map((record) => ({ ...record })),
340
+ setByOwner: (key, value, owner) => {
341
+ applyOwnerUpdate(key, value, owner);
342
+ },
343
+ requestChange: (request) => {
344
+ const current = records.get(request.key);
345
+ if (!current) {
346
+ throw new Error(`sharedState key "${request.key}" is not registered.`);
347
+ }
348
+ if (request.requestedBy === current.owner) {
349
+ applyOwnerUpdate(request.key, request.nextValue, current.owner);
350
+ return;
351
+ }
352
+ const listeners = requestListenersByKey.get(request.key);
353
+ if (!listeners || listeners.size === 0) {
354
+ throw new Error(
355
+ `No owner request handler for key "${request.key}". Owner is "${current.owner}".`
356
+ );
357
+ }
358
+ let handled = false;
359
+ for (const sub of listeners) {
360
+ if (sub.owner === current.owner) {
361
+ handled = true;
362
+ sub.listener(request);
363
+ }
364
+ }
365
+ if (!handled) {
366
+ throw new Error(
367
+ `Owner "${current.owner}" has no active request handler for key "${request.key}".`
368
+ );
369
+ }
370
+ },
371
+ subscribeKey: (key, listener) => {
372
+ let listeners = listenersByKey.get(key);
373
+ if (!listeners) {
374
+ listeners = /* @__PURE__ */ new Set();
375
+ listenersByKey.set(key, listeners);
376
+ }
377
+ listeners.add(listener);
378
+ return () => {
379
+ const current = listenersByKey.get(key);
380
+ current?.delete(listener);
381
+ if (current && current.size === 0) {
382
+ listenersByKey.delete(key);
383
+ }
384
+ };
385
+ },
386
+ subscribeRequests: (key, owner, listener) => {
387
+ let listeners = requestListenersByKey.get(key);
388
+ if (!listeners) {
389
+ listeners = /* @__PURE__ */ new Set();
390
+ requestListenersByKey.set(key, listeners);
391
+ }
392
+ const sub = {
393
+ owner,
394
+ listener
395
+ };
396
+ listeners.add(sub);
397
+ return () => {
398
+ const current = requestListenersByKey.get(key);
399
+ current?.delete(sub);
400
+ if (current && current.size === 0) {
401
+ requestListenersByKey.delete(key);
402
+ }
403
+ };
404
+ }
405
+ };
406
+ }
407
+
408
+ // src/services/user.ts
409
+ function createStandaloneUserService(user) {
410
+ const listeners = /* @__PURE__ */ new Set();
411
+ const currentUser = user;
412
+ return {
413
+ getSnapshot: () => currentUser,
414
+ subscribe: (listener) => {
415
+ listeners.add(listener);
416
+ return () => listeners.delete(listener);
417
+ }
418
+ };
419
+ }
420
+
421
+ // src/initStandaloneServices.ts
422
+ function initStandaloneServices(options = {}) {
423
+ const {
424
+ apiBase = "http://localhost:4000",
425
+ lang = "vi",
426
+ user = { id: "dev", name: "Dev User" },
427
+ customize,
428
+ registerMode = "if-missing"
429
+ } = options;
430
+ const services = {
431
+ i18n: createStandaloneI18nService(lang),
432
+ userService: createStandaloneUserService(user ?? null),
433
+ apiClient: createStandaloneApiClient(apiBase),
434
+ navigation: createStandaloneNavigationService("/standalone"),
435
+ configService: createStandaloneConfigService(
436
+ { [FEATURE_FLAGS2.uiExperimental]: true },
437
+ { envName: "standalone", apiBase }
438
+ ),
439
+ permissionService: createStandalonePermissionService(),
440
+ sharedState: createStandaloneSharedStateService(),
441
+ observability: createStandaloneObservabilityService(),
442
+ notification: createStandaloneNotificationService(),
443
+ analytics: createStandaloneAnalyticsService()
444
+ };
445
+ customize?.(services);
446
+ services.sharedState.ensureKey(SHARED_STATE_KEYS.x, "host", 0);
447
+ services.sharedState.subscribeRequests(SHARED_STATE_KEYS.x, "host", (request) => {
448
+ services.sharedState.setByOwner(SHARED_STATE_KEYS.x, request.nextValue, "host", request.reason);
449
+ services.observability.log({
450
+ level: "info",
451
+ source: "standalone.sharedState",
452
+ message: `Applied shared.x from request by ${request.requestedBy}`,
453
+ data: { nextValue: request.nextValue, reason: request.reason ?? "" }
454
+ });
455
+ });
456
+ if (registerMode === "always" || !getService(TOKENS.i18n)) {
457
+ registerService(TOKENS.i18n, services.i18n);
458
+ }
459
+ if (registerMode === "always" || !getService(TOKENS.userService)) {
460
+ registerService(TOKENS.userService, services.userService);
461
+ }
462
+ if (registerMode === "always" || !getService(TOKENS.apiClient)) {
463
+ registerService(TOKENS.apiClient, services.apiClient);
464
+ }
465
+ if (registerMode === "always" || !getService(TOKENS.navigation)) {
466
+ registerService(TOKENS.navigation, services.navigation);
467
+ }
468
+ if (registerMode === "always" || !getService(TOKENS.configService)) {
469
+ registerService(TOKENS.configService, services.configService);
470
+ }
471
+ if (registerMode === "always" || !getService(TOKENS.permissionService)) {
472
+ registerService(TOKENS.permissionService, services.permissionService);
473
+ }
474
+ if (registerMode === "always" || !getService(TOKENS.sharedState)) {
475
+ registerService(TOKENS.sharedState, services.sharedState);
476
+ }
477
+ if (registerMode === "always" || !getService(TOKENS.observability)) {
478
+ registerService(TOKENS.observability, services.observability);
479
+ }
480
+ if (registerMode === "always" || !getService(TOKENS.notification)) {
481
+ registerService(TOKENS.notification, services.notification);
482
+ }
483
+ if (registerMode === "always" || !getService(TOKENS.analytics)) {
484
+ registerService(TOKENS.analytics, services.analytics);
485
+ }
486
+ }
487
+ export {
488
+ createStandaloneApiClient,
489
+ createStandaloneI18nService,
490
+ createStandaloneUserService,
491
+ initStandaloneServices
492
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "ptech-shell-dev",
3
+ "version": "0.1.0",
4
+ "description": "Standalone/mock shell service implementations for Module Federation apps.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format esm --dts --out-dir dist --clean",
21
+ "dev": "tsup src/index.ts --format esm --dts --out-dir dist --watch",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "dependencies": {
25
+ "ptech-shell-sdk": "^0.1.0"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=18"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "^19.0.0",
32
+ "react": "^19.0.0",
33
+ "tsup": "^8.0.1",
34
+ "typescript": "^5.6.2"
35
+ },
36
+ "keywords": [
37
+ "micro-frontend",
38
+ "module-federation",
39
+ "shell",
40
+ "standalone",
41
+ "mock"
42
+ ],
43
+ "license": "MIT"
44
+ }