@techdentalkart/features 2.0.1
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 +167 -0
- package/dist/context.d.ts +79 -0
- package/dist/context.js +64 -0
- package/dist/exposures.d.ts +59 -0
- package/dist/exposures.js +0 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +152 -0
- package/dist/institute-management/index.d.ts +28 -0
- package/dist/institute-management/index.js +48 -0
- package/dist/nest/index.d.ts +44 -0
- package/dist/nest/index.js +124 -0
- package/dist/next/index.d.ts +45 -0
- package/dist/next/index.js +102 -0
- package/dist/providers/file.d.ts +18 -0
- package/dist/providers/file.js +76 -0
- package/dist/providers/growthbook.d.ts +46 -0
- package/dist/providers/growthbook.js +143 -0
- package/dist/providers/off.d.ts +10 -0
- package/dist/providers/off.js +15 -0
- package/dist/providers/provider.d.ts +10 -0
- package/dist/providers/provider.js +2 -0
- package/dist/react/index.d.ts +50 -0
- package/dist/react/index.js +104 -0
- package/dist/website/index.d.ts +55 -0
- package/dist/website/index.js +84 -0
- package/package.json +98 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type Flag, type FlagSet } from "../context";
|
|
3
|
+
export type { Flag, FlagSet } from "../context";
|
|
4
|
+
/** AsyncStorage, localStorage, or anything with the same two methods. */
|
|
5
|
+
export interface FlagStorage {
|
|
6
|
+
getItem(key: string): Promise<string | null> | string | null;
|
|
7
|
+
setItem(key: string, value: string): Promise<void> | void;
|
|
8
|
+
}
|
|
9
|
+
/** Where a browser or app reports "this visitor saw this variant", and who the visitor is. */
|
|
10
|
+
export interface ExposureReporting {
|
|
11
|
+
/** the surface's own route that forwards to the engine, e.g. `/api/exposures` or `customer/api/v1/exposures` */
|
|
12
|
+
endpoint: string;
|
|
13
|
+
/** the id experiments are hashed on (dk_vid on the web, the device id in the app) */
|
|
14
|
+
unit: string;
|
|
15
|
+
unitType?: string;
|
|
16
|
+
platform?: string;
|
|
17
|
+
surface?: string;
|
|
18
|
+
/** a few scalar facts for breakdowns (device type, customer type ...); never anything personal */
|
|
19
|
+
attrs?: Record<string, string | number | boolean>;
|
|
20
|
+
}
|
|
21
|
+
export interface FeaturesProviderProps {
|
|
22
|
+
/** Flags already resolved on the server for this visitor (Next.js). First paint is right, no flash. */
|
|
23
|
+
initial?: FlagSet;
|
|
24
|
+
/** How to fetch a fresh set (the app: `() => api.get("/features")`). */
|
|
25
|
+
fetch?: () => Promise<FlagSet | {
|
|
26
|
+
flags: FlagSet;
|
|
27
|
+
}>;
|
|
28
|
+
/** Refresh interval when `fetch` is given. Default 5 minutes. 0 disables the timer. */
|
|
29
|
+
refreshMs?: number;
|
|
30
|
+
/** Last-known-good across launches (the app: AsyncStorage). Optional. */
|
|
31
|
+
storage?: FlagStorage;
|
|
32
|
+
/** Subscribe to "app came back to the foreground"; return an unsubscribe. RN: AppState. Web: automatic. */
|
|
33
|
+
resume?: (onResume: () => void) => () => void;
|
|
34
|
+
/** Enables exposure reporting from `useFlag`. Omit and nothing is ever sent. */
|
|
35
|
+
exposure?: ExposureReporting;
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The one provider every React surface uses: Next client tree (fed by the server) and React Native
|
|
40
|
+
* (fed by `fetch`). Keeps the last good answer if a refresh fails. Flags are read with `useFlag(name)`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function FeaturesProvider({ initial, fetch, refreshMs, storage, resume, exposure, children }: FeaturesProviderProps): React.JSX.Element;
|
|
43
|
+
/**
|
|
44
|
+
* `{ on, ...attributes }` for one flag. `{ on: false }` when unknown, off, or outside the provider.
|
|
45
|
+
* Reading a flag that an experiment decided reports the exposure once the component is on screen
|
|
46
|
+
* (never during server rendering).
|
|
47
|
+
*/
|
|
48
|
+
export declare function useFlag(name: string): Flag;
|
|
49
|
+
/** Every flag for this visitor. For code that must branch on several at once. No exposures. */
|
|
50
|
+
export declare function useFlags(): FlagSet;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.FeaturesProvider = FeaturesProvider;
|
|
5
|
+
exports.useFlag = useFlag;
|
|
6
|
+
exports.useFlags = useFlags;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
const react_1 = require("react");
|
|
9
|
+
const context_1 = require("../context");
|
|
10
|
+
const exposures_1 = require("../exposures");
|
|
11
|
+
const STORAGE_KEY = "dk_flags_v1";
|
|
12
|
+
const FlagsContext = (0, react_1.createContext)({});
|
|
13
|
+
const ReporterContext = (0, react_1.createContext)(null);
|
|
14
|
+
/**
|
|
15
|
+
* The one provider every React surface uses: Next client tree (fed by the server) and React Native
|
|
16
|
+
* (fed by `fetch`). Keeps the last good answer if a refresh fails. Flags are read with `useFlag(name)`.
|
|
17
|
+
*/
|
|
18
|
+
function FeaturesProvider({ initial, fetch, refreshMs = 300000, storage, resume, exposure, children }) {
|
|
19
|
+
const [flags, setFlags] = (0, react_1.useState)(initial ?? {});
|
|
20
|
+
const alive = (0, react_1.useRef)(true);
|
|
21
|
+
// last-known-good from storage, only when nothing better was given
|
|
22
|
+
(0, react_1.useEffect)(() => {
|
|
23
|
+
if (initial || !storage)
|
|
24
|
+
return;
|
|
25
|
+
Promise.resolve(storage.getItem(STORAGE_KEY)).then((s) => {
|
|
26
|
+
if (s && alive.current) {
|
|
27
|
+
try {
|
|
28
|
+
setFlags(JSON.parse(s));
|
|
29
|
+
}
|
|
30
|
+
catch { /* ignore a bad cache */ }
|
|
31
|
+
}
|
|
32
|
+
}).catch(() => { });
|
|
33
|
+
}, [initial, storage]);
|
|
34
|
+
(0, react_1.useEffect)(() => {
|
|
35
|
+
alive.current = true;
|
|
36
|
+
if (!fetch)
|
|
37
|
+
return;
|
|
38
|
+
const refresh = () => fetch().then((r) => {
|
|
39
|
+
const next = r?.flags ?? r;
|
|
40
|
+
if (!next || typeof next !== "object" || !alive.current)
|
|
41
|
+
return;
|
|
42
|
+
setFlags(next);
|
|
43
|
+
if (storage)
|
|
44
|
+
Promise.resolve(storage.setItem(STORAGE_KEY, JSON.stringify(next))).catch(() => { });
|
|
45
|
+
}).catch(() => { });
|
|
46
|
+
refresh();
|
|
47
|
+
const timer = refreshMs > 0 ? setInterval(refresh, refreshMs) : undefined;
|
|
48
|
+
let unsubscribe;
|
|
49
|
+
if (resume)
|
|
50
|
+
unsubscribe = resume(refresh);
|
|
51
|
+
else if (typeof document !== "undefined") {
|
|
52
|
+
const onVisible = () => { if (document.visibilityState === "visible")
|
|
53
|
+
refresh(); };
|
|
54
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
55
|
+
unsubscribe = () => document.removeEventListener("visibilitychange", onVisible);
|
|
56
|
+
}
|
|
57
|
+
return () => { alive.current = false; if (timer)
|
|
58
|
+
clearInterval(timer); unsubscribe?.(); };
|
|
59
|
+
}, [fetch, refreshMs, storage, resume]);
|
|
60
|
+
// one queue per provider; recreated only when the endpoint or the visitor changes
|
|
61
|
+
const reporter = (0, react_1.useMemo)(() => {
|
|
62
|
+
if (!exposure)
|
|
63
|
+
return null;
|
|
64
|
+
const queue = new exposures_1.ExposureQueue({ send: (0, exposures_1.httpSender)(exposure.endpoint), flushMs: 2000 });
|
|
65
|
+
return (flag) => {
|
|
66
|
+
if (flag.experimentId === undefined || flag.variationId === undefined)
|
|
67
|
+
return;
|
|
68
|
+
const e = {
|
|
69
|
+
experimentId: flag.experimentId,
|
|
70
|
+
variationId: flag.variationId,
|
|
71
|
+
unit: exposure.unit,
|
|
72
|
+
unitType: exposure.unitType ?? "anonymousId",
|
|
73
|
+
ts: new Date().toISOString(),
|
|
74
|
+
platform: exposure.platform,
|
|
75
|
+
surface: exposure.surface,
|
|
76
|
+
attrs: exposure.attrs,
|
|
77
|
+
};
|
|
78
|
+
queue.record(e);
|
|
79
|
+
};
|
|
80
|
+
}, [exposure]);
|
|
81
|
+
const value = (0, react_1.useMemo)(() => flags, [flags]);
|
|
82
|
+
return ((0, jsx_runtime_1.jsx)(ReporterContext.Provider, { value: reporter, children: (0, jsx_runtime_1.jsx)(FlagsContext.Provider, { value: value, children: children }) }));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* `{ on, ...attributes }` for one flag. `{ on: false }` when unknown, off, or outside the provider.
|
|
86
|
+
* Reading a flag that an experiment decided reports the exposure once the component is on screen
|
|
87
|
+
* (never during server rendering).
|
|
88
|
+
*/
|
|
89
|
+
function useFlag(name) {
|
|
90
|
+
const flag = (0, react_1.useContext)(FlagsContext)[name] ?? context_1.OFF;
|
|
91
|
+
const report = (0, react_1.useContext)(ReporterContext);
|
|
92
|
+
const experimentId = flag.experimentId;
|
|
93
|
+
const variationId = flag.variationId;
|
|
94
|
+
(0, react_1.useEffect)(() => {
|
|
95
|
+
if (report && experimentId !== undefined)
|
|
96
|
+
report(flag);
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
98
|
+
}, [report, name, experimentId, variationId]);
|
|
99
|
+
return flag;
|
|
100
|
+
}
|
|
101
|
+
/** Every flag for this visitor. For code that must branch on several at once. No exposures. */
|
|
102
|
+
function useFlags() {
|
|
103
|
+
return (0, react_1.useContext)(FlagsContext);
|
|
104
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { FeatureContext } from "../context";
|
|
2
|
+
/** What a rule may target on the website. Build it once per request in `lib/features.ts` and pass it here. */
|
|
3
|
+
export interface WebsiteFacts {
|
|
4
|
+
/** the signed-in customer, from `customers/me` with the visitor's own token; omit when signed out */
|
|
5
|
+
customer?: {
|
|
6
|
+
id?: number | string;
|
|
7
|
+
email?: string;
|
|
8
|
+
type?: string;
|
|
9
|
+
speciality?: string;
|
|
10
|
+
no_of_orders?: number;
|
|
11
|
+
group_code?: string;
|
|
12
|
+
} | null;
|
|
13
|
+
/** first-party anonymous id (the `dk_vid` cookie or the `x-dk-vid` header): the unit for rollouts and experiments */
|
|
14
|
+
anonymousId?: string;
|
|
15
|
+
/** the `pincode` cookie: where the visitor browses from */
|
|
16
|
+
pincode?: string;
|
|
17
|
+
/** district and state of that pincode, when the app has looked them up */
|
|
18
|
+
district?: string;
|
|
19
|
+
state?: string;
|
|
20
|
+
/** the User-Agent header, for deviceType and bots */
|
|
21
|
+
userAgent?: string;
|
|
22
|
+
/** the `x-instadent-mode` header */
|
|
23
|
+
instadent?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** WebsiteFacts -> FeatureContext. The keys a rule sees: anonymousId, userId, email, isLoggedIn, isStaff, isBot,
|
|
26
|
+
* customerType, speciality, groupCode, noOfOrders, pincode, district, state, platform, deviceType, instadent. */
|
|
27
|
+
export declare function websiteContext(f: WebsiteFacts): FeatureContext;
|
|
28
|
+
/** The website's flags. Names here, nowhere else. */
|
|
29
|
+
export declare const WEBSITE_FLAGS: {
|
|
30
|
+
/** number: percent added to the catalogue prices shown. Display only until the cart service reads the same flag. */
|
|
31
|
+
readonly priceMarkup: "pricing.markup";
|
|
32
|
+
/** kill switch by place: `message` explains why payment is paused. */
|
|
33
|
+
readonly paymentDisabled: "checkout.payment_disabled";
|
|
34
|
+
/** release: the market ticker strip in the header. */
|
|
35
|
+
readonly stockTicker: "header.stock_ticker";
|
|
36
|
+
};
|
|
37
|
+
export interface PricingVariant {
|
|
38
|
+
/** percent added to shown prices; 0 when no rule matched */
|
|
39
|
+
percent: number;
|
|
40
|
+
experimentId?: string;
|
|
41
|
+
variationId?: number;
|
|
42
|
+
/** a catalogue price as it should be shown to this visitor */
|
|
43
|
+
apply<T extends number | null | undefined>(price: T): T;
|
|
44
|
+
}
|
|
45
|
+
/** The pricing decision for this visitor. Reading it reports the experiment exposure. */
|
|
46
|
+
export declare function usePricingVariant(): PricingVariant;
|
|
47
|
+
/** A catalogue price as it should be shown to this visitor. Identity when no markup applies. */
|
|
48
|
+
export declare function useMarkedUpPrice<T extends number | null | undefined>(price: T): T;
|
|
49
|
+
/** Whether payment is paused for this visitor, and what to tell them. */
|
|
50
|
+
export declare function usePaymentBlock(): {
|
|
51
|
+
blocked: boolean;
|
|
52
|
+
message: string;
|
|
53
|
+
};
|
|
54
|
+
/** Whether to show the header stock ticker. */
|
|
55
|
+
export declare function useStockTicker(): boolean;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WEBSITE_FLAGS = void 0;
|
|
4
|
+
exports.websiteContext = websiteContext;
|
|
5
|
+
exports.usePricingVariant = usePricingVariant;
|
|
6
|
+
exports.useMarkedUpPrice = useMarkedUpPrice;
|
|
7
|
+
exports.usePaymentBlock = usePaymentBlock;
|
|
8
|
+
exports.useStockTicker = useStockTicker;
|
|
9
|
+
// No "use client" here on purpose: the context builder runs on the server inside getFlags(), and a
|
|
10
|
+
// client-marked module would turn it into a client reference that cannot be called there. The hooks
|
|
11
|
+
// below still work in client components, which import this module across the boundary themselves.
|
|
12
|
+
/**
|
|
13
|
+
* The website's declarations: what it sends to the rules, and what it reads back.
|
|
14
|
+
* This is the only place flag names for the website are written down.
|
|
15
|
+
*
|
|
16
|
+
* Engine keys: `website (staging)` for dk-frontend.dentalkart.com, `website (production)` for the live site.
|
|
17
|
+
*/
|
|
18
|
+
const react_1 = require("../react");
|
|
19
|
+
/** Free-text columns: trim, collapse spaces, lowercase. Rules are written in lowercase. */
|
|
20
|
+
const normalise = (v) => {
|
|
21
|
+
if (typeof v !== "string")
|
|
22
|
+
return undefined;
|
|
23
|
+
const s = v.trim().replace(/\s+/g, " ").toLowerCase();
|
|
24
|
+
return s || undefined;
|
|
25
|
+
};
|
|
26
|
+
const BOT = /bot|crawl|spider|slurp|facebookexternalhit|preview|headless/i;
|
|
27
|
+
/** WebsiteFacts -> FeatureContext. The keys a rule sees: anonymousId, userId, email, isLoggedIn, isStaff, isBot,
|
|
28
|
+
* customerType, speciality, groupCode, noOfOrders, pincode, district, state, platform, deviceType, instadent. */
|
|
29
|
+
function websiteContext(f) {
|
|
30
|
+
const c = f.customer ?? undefined;
|
|
31
|
+
const userId = c?.id != null ? String(c.id) : undefined;
|
|
32
|
+
return {
|
|
33
|
+
anonymousId: f.anonymousId,
|
|
34
|
+
targetingKey: f.anonymousId ?? userId,
|
|
35
|
+
platform: "web",
|
|
36
|
+
deviceType: f.userAgent && /Mobi|Android|iPhone/i.test(f.userAgent) ? "mobile" : "desktop",
|
|
37
|
+
isBot: !!f.userAgent && BOT.test(f.userAgent),
|
|
38
|
+
instadent: !!f.instadent,
|
|
39
|
+
pincode: f.pincode?.trim() || undefined,
|
|
40
|
+
district: f.district,
|
|
41
|
+
state: f.state,
|
|
42
|
+
userId,
|
|
43
|
+
email: c?.email,
|
|
44
|
+
isLoggedIn: !!userId,
|
|
45
|
+
isStaff: !!c?.email?.toLowerCase().endsWith("@dentalkart.com"),
|
|
46
|
+
customerType: normalise(c?.type),
|
|
47
|
+
speciality: normalise(c?.speciality),
|
|
48
|
+
groupCode: normalise(c?.group_code) ?? (userId ? undefined : "not logged in"),
|
|
49
|
+
noOfOrders: typeof c?.no_of_orders === "number" ? c.no_of_orders : undefined,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** The website's flags. Names here, nowhere else. */
|
|
53
|
+
exports.WEBSITE_FLAGS = {
|
|
54
|
+
/** number: percent added to the catalogue prices shown. Display only until the cart service reads the same flag. */
|
|
55
|
+
priceMarkup: "pricing.markup",
|
|
56
|
+
/** kill switch by place: `message` explains why payment is paused. */
|
|
57
|
+
paymentDisabled: "checkout.payment_disabled",
|
|
58
|
+
/** release: the market ticker strip in the header. */
|
|
59
|
+
stockTicker: "header.stock_ticker",
|
|
60
|
+
};
|
|
61
|
+
/** The pricing decision for this visitor. Reading it reports the experiment exposure. */
|
|
62
|
+
function usePricingVariant() {
|
|
63
|
+
const f = (0, react_1.useFlag)(exports.WEBSITE_FLAGS.priceMarkup);
|
|
64
|
+
const percent = f.on && typeof f.value === "number" && f.value > 0 ? f.value : 0;
|
|
65
|
+
return {
|
|
66
|
+
percent,
|
|
67
|
+
experimentId: f.experimentId,
|
|
68
|
+
variationId: f.variationId,
|
|
69
|
+
apply: (price) => typeof price !== "number" || percent === 0 ? price : (Math.round(price * (1 + percent / 100) * 100) / 100),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** A catalogue price as it should be shown to this visitor. Identity when no markup applies. */
|
|
73
|
+
function useMarkedUpPrice(price) {
|
|
74
|
+
return usePricingVariant().apply(price);
|
|
75
|
+
}
|
|
76
|
+
/** Whether payment is paused for this visitor, and what to tell them. */
|
|
77
|
+
function usePaymentBlock() {
|
|
78
|
+
const f = (0, react_1.useFlag)(exports.WEBSITE_FLAGS.paymentDisabled);
|
|
79
|
+
return { blocked: f.on, message: (typeof f.message === "string" && f.message) || "Online payment is paused for your area right now." };
|
|
80
|
+
}
|
|
81
|
+
/** Whether to show the header stock ticker. */
|
|
82
|
+
function useStockTicker() {
|
|
83
|
+
return (0, react_1.useFlag)(exports.WEBSITE_FLAGS.stockTicker).on;
|
|
84
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@techdentalkart/features",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Dentalkart feature flags and experiments. One package, one object ({ on, value?, ...attributes, experimentId? }), one hook (useFlag). Same shape in every repo: Node, NestJS, Next.js, React Native. Rules come from the Feature Management & A/B Testing section of the admin panel.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./react": {
|
|
13
|
+
"types": "./dist/react/index.d.ts",
|
|
14
|
+
"default": "./dist/react/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./next": {
|
|
17
|
+
"types": "./dist/next/index.d.ts",
|
|
18
|
+
"default": "./dist/next/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./nest": {
|
|
21
|
+
"types": "./dist/nest/index.d.ts",
|
|
22
|
+
"default": "./dist/nest/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./website": {
|
|
25
|
+
"types": "./dist/website/index.d.ts",
|
|
26
|
+
"default": "./dist/website/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./institute-management": {
|
|
29
|
+
"types": "./dist/institute-management/index.d.ts",
|
|
30
|
+
"default": "./dist/institute-management/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"typesVersions": {
|
|
34
|
+
"*": {
|
|
35
|
+
"react": [
|
|
36
|
+
"dist/react/index.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"next": [
|
|
39
|
+
"dist/next/index.d.ts"
|
|
40
|
+
],
|
|
41
|
+
"nest": [
|
|
42
|
+
"dist/nest/index.d.ts"
|
|
43
|
+
],
|
|
44
|
+
"website": [
|
|
45
|
+
"dist/website/index.d.ts"
|
|
46
|
+
],
|
|
47
|
+
"institute-management": [
|
|
48
|
+
"dist/institute-management/index.d.ts"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"README.md"
|
|
55
|
+
],
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
58
|
+
"test": "npm run build && node --test test/*.test.js"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@growthbook/growthbook": "^1.7.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"react": ">=18",
|
|
65
|
+
"next": ">=14",
|
|
66
|
+
"@nestjs/common": ">=8",
|
|
67
|
+
"reflect-metadata": ">=0.1.13"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"react": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"next": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@nestjs/common": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"reflect-metadata": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@nestjs/common": "^10.4.0",
|
|
85
|
+
"@types/node": "^20.14.0",
|
|
86
|
+
"@types/react": "^18.3.0",
|
|
87
|
+
"next": "^14.1.0",
|
|
88
|
+
"react": "^18.3.0",
|
|
89
|
+
"react-dom": "^18.3.0",
|
|
90
|
+
"reflect-metadata": "^0.2.2",
|
|
91
|
+
"rxjs": "^7.8.0",
|
|
92
|
+
"typescript": "^5.6.0"
|
|
93
|
+
},
|
|
94
|
+
"publishConfig": {
|
|
95
|
+
"@techdentalkart:registry": "https://gitlab.com/api/v4/projects/86582456/packages/npm/"
|
|
96
|
+
},
|
|
97
|
+
"license": "UNLICENSED"
|
|
98
|
+
}
|