@offlinejs/utils 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.
@@ -0,0 +1,39 @@
1
+ import { IndexDefinition, EntityRecord, QueryOptions, StorageAdapter, SyncTransport, QueryFilters, IndexableStorageAdapter } from '@offlinejs/types';
2
+
3
+ declare const now: () => number;
4
+ declare const isBrowser: () => boolean;
5
+ declare const delay: (ms: number) => Promise<void>;
6
+ declare const createId: () => string;
7
+ declare const clone: <TValue>(value: TValue) => TValue;
8
+ declare const normalizeError: (error: unknown) => Error;
9
+ declare const assertStorageAdapter: (adapter: StorageAdapter) => void;
10
+ declare const assertSyncTransport: (transport: SyncTransport) => void;
11
+ declare const toQueryString: (query?: Record<string, string | number | boolean | undefined>) => string;
12
+ declare const backoffDelay: (attempt: number, options: {
13
+ baseDelayMs: number;
14
+ factor: number;
15
+ jitter: boolean;
16
+ maxDelayMs: number;
17
+ }) => number;
18
+ declare const matchesQuery: <TRecord extends EntityRecord>(record: TRecord, query?: QueryOptions<TRecord>) => boolean;
19
+ declare const applyQuery: <TRecord extends EntityRecord>(records: TRecord[], query?: QueryOptions<TRecord>) => TRecord[];
20
+ declare const countQuery: <TRecord extends EntityRecord>(records: TRecord[], query?: QueryOptions<TRecord>) => number;
21
+ declare const serializeIndexValue: (value: unknown) => string;
22
+ declare const serializeCompoundIndexValue: (values: unknown[]) => string;
23
+ declare const readIndexFields: (record: EntityRecord, fields: Array<string | number | symbol>) => unknown[];
24
+ interface EqualityLookup {
25
+ field: string;
26
+ value: unknown;
27
+ }
28
+ /** Extract simple equality lookups that secondary indexes can accelerate. */
29
+ declare const getEqualityFilterLookups: <TRecord extends EntityRecord>(filters?: QueryFilters<TRecord>) => EqualityLookup[];
30
+ interface MatchedIndex {
31
+ index: IndexDefinition;
32
+ values: unknown[];
33
+ }
34
+ /** Prefer the most specific index whose fields are all covered by equality filters. */
35
+ declare const findMatchingIndex: (indexes: IndexDefinition[], lookups: EqualityLookup[]) => MatchedIndex | null;
36
+ /** Forward index APIs through storage wrappers (validation/encryption). */
37
+ declare const withIndexForwarding: <TAdapter extends StorageAdapter>(wrapper: TAdapter, inner: StorageAdapter) => TAdapter & IndexableStorageAdapter;
38
+
39
+ export { type EqualityLookup, type MatchedIndex, applyQuery, assertStorageAdapter, assertSyncTransport, backoffDelay, clone, countQuery, createId, delay, findMatchingIndex, getEqualityFilterLookups, isBrowser, matchesQuery, normalizeError, now, readIndexFields, serializeCompoundIndexValue, serializeIndexValue, toQueryString, withIndexForwarding };
package/dist/index.js ADDED
@@ -0,0 +1,229 @@
1
+ import { STORAGE_ADAPTER_CONTRACT_VERSION, SYNC_TRANSPORT_CONTRACT_VERSION } from '@offlinejs/types';
2
+
3
+ // src/index.ts
4
+ var now = () => Date.now();
5
+ var isBrowser = () => typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
6
+ var delay = (ms) => new Promise((resolve) => {
7
+ globalThis.setTimeout(resolve, ms);
8
+ });
9
+ var createId = () => {
10
+ const cryptoApi = globalThis.crypto;
11
+ if (cryptoApi?.randomUUID) {
12
+ return cryptoApi.randomUUID();
13
+ }
14
+ return `offline_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
15
+ };
16
+ var clone = (value) => {
17
+ if (typeof globalThis.structuredClone === "function") {
18
+ return globalThis.structuredClone(value);
19
+ }
20
+ return JSON.parse(JSON.stringify(value));
21
+ };
22
+ var normalizeError = (error) => {
23
+ if (error instanceof Error) {
24
+ return error;
25
+ }
26
+ return new Error(typeof error === "string" ? error : "Unknown OfflineJS error");
27
+ };
28
+ var assertStorageAdapter = (adapter) => {
29
+ const requiredMethods = [
30
+ "get",
31
+ "set",
32
+ "delete",
33
+ "find",
34
+ "clear",
35
+ "transaction"
36
+ ];
37
+ if (!adapter.name) {
38
+ throw new Error("OfflineJS storage adapter requires a stable name");
39
+ }
40
+ for (const method of requiredMethods) {
41
+ if (typeof adapter[method] !== "function") {
42
+ throw new Error(`OfflineJS storage adapter "${adapter.name}" is missing ${String(method)}()`);
43
+ }
44
+ }
45
+ if (adapter.contractVersion !== void 0 && adapter.contractVersion !== STORAGE_ADAPTER_CONTRACT_VERSION) {
46
+ throw new Error(
47
+ `Unsupported storage adapter contract ${adapter.contractVersion}; expected ${STORAGE_ADAPTER_CONTRACT_VERSION}`
48
+ );
49
+ }
50
+ };
51
+ var assertSyncTransport = (transport) => {
52
+ if (typeof transport.request !== "function") {
53
+ throw new Error("OfflineJS sync transport requires request()");
54
+ }
55
+ if (transport.contractVersion !== void 0 && transport.contractVersion !== SYNC_TRANSPORT_CONTRACT_VERSION) {
56
+ throw new Error(
57
+ `Unsupported sync transport contract ${transport.contractVersion}; expected ${SYNC_TRANSPORT_CONTRACT_VERSION}`
58
+ );
59
+ }
60
+ };
61
+ var toQueryString = (query) => {
62
+ if (!query) {
63
+ return "";
64
+ }
65
+ const params = new URLSearchParams();
66
+ for (const [key, value] of Object.entries(query)) {
67
+ if (value !== void 0) {
68
+ params.set(key, String(value));
69
+ }
70
+ }
71
+ const result = params.toString();
72
+ return result.length > 0 ? `?${result}` : "";
73
+ };
74
+ var backoffDelay = (attempt, options) => {
75
+ const exponentialDelay = Math.min(
76
+ options.baseDelayMs * options.factor ** Math.max(0, attempt - 1),
77
+ options.maxDelayMs
78
+ );
79
+ if (!options.jitter) {
80
+ return exponentialDelay;
81
+ }
82
+ return Math.round(exponentialDelay * (0.5 + Math.random() * 0.5));
83
+ };
84
+ var matchesQuery = (record, query = {}) => {
85
+ if (query.filters && !matchesFilters(record, query.filters)) {
86
+ return false;
87
+ }
88
+ if (!query.search) {
89
+ return true;
90
+ }
91
+ const search = query.search.toLowerCase();
92
+ const fields = query.searchFields?.map((field) => String(field)) ?? Object.keys(record);
93
+ return fields.some(
94
+ (field) => String(record[field] ?? "").toLowerCase().includes(search)
95
+ );
96
+ };
97
+ var applyQuery = (records, query = {}) => {
98
+ const filtered = records.filter((record) => matchesQuery(record, query));
99
+ const sorted = sortRecords(filtered, query);
100
+ const offset = Math.max(0, query.offset ?? 0);
101
+ const limit = query.limit ?? sorted.length;
102
+ return sorted.slice(offset, offset + limit);
103
+ };
104
+ var countQuery = (records, query = {}) => records.filter((record) => matchesQuery(record, query)).length;
105
+ var serializeIndexValue = (value) => JSON.stringify(value ?? null);
106
+ var serializeCompoundIndexValue = (values) => JSON.stringify(values.map((value) => value ?? null));
107
+ var readIndexFields = (record, fields) => fields.map((field) => record[String(field)]);
108
+ var getEqualityFilterLookups = (filters) => {
109
+ if (!filters) {
110
+ return [];
111
+ }
112
+ const lookups = [];
113
+ for (const [field, expected] of Object.entries(filters)) {
114
+ if (expected === void 0) {
115
+ continue;
116
+ }
117
+ if (Array.isArray(expected) || expected === null || typeof expected !== "object") {
118
+ lookups.push({ field, value: expected });
119
+ continue;
120
+ }
121
+ if ("eq" in expected && expected.eq !== void 0) {
122
+ lookups.push({ field, value: expected.eq });
123
+ }
124
+ }
125
+ return lookups;
126
+ };
127
+ var findMatchingIndex = (indexes, lookups) => {
128
+ if (indexes.length === 0 || lookups.length === 0) {
129
+ return null;
130
+ }
131
+ const lookupByField = new Map(lookups.map((lookup) => [lookup.field, lookup.value]));
132
+ const ranked = [...indexes].sort((left, right) => right.fields.length - left.fields.length);
133
+ for (const index of ranked) {
134
+ const fields = index.fields.map(String);
135
+ if (fields.every((field) => lookupByField.has(field))) {
136
+ return {
137
+ index,
138
+ values: fields.map((field) => lookupByField.get(field))
139
+ };
140
+ }
141
+ }
142
+ return null;
143
+ };
144
+ var withIndexForwarding = (wrapper, inner) => {
145
+ const indexable = inner;
146
+ return Object.assign(wrapper, {
147
+ ...typeof indexable.createIndex === "function" ? { createIndex: indexable.createIndex.bind(indexable) } : {},
148
+ ...typeof indexable.dropIndex === "function" ? { dropIndex: indexable.dropIndex.bind(indexable) } : {},
149
+ ...typeof indexable.listIndexes === "function" ? { listIndexes: indexable.listIndexes.bind(indexable) } : {}
150
+ });
151
+ };
152
+ var sortRecords = (records, query) => {
153
+ if (!query.orderBy) {
154
+ return [...records];
155
+ }
156
+ const direction = query.sort === "desc" ? -1 : 1;
157
+ const key = String(query.orderBy);
158
+ return [...records].sort((left, right) => {
159
+ const leftValue = left[key];
160
+ const rightValue = right[key];
161
+ if (leftValue === rightValue) {
162
+ return 0;
163
+ }
164
+ if (leftValue === void 0 || leftValue === null) {
165
+ return -1 * direction;
166
+ }
167
+ if (rightValue === void 0 || rightValue === null) {
168
+ return direction;
169
+ }
170
+ return leftValue > rightValue ? direction : -direction;
171
+ });
172
+ };
173
+ var matchesFilters = (record, filters) => {
174
+ for (const [field, expected] of Object.entries(filters)) {
175
+ const actual = record[field];
176
+ if (Array.isArray(expected)) {
177
+ if (!expected.includes(actual)) {
178
+ return false;
179
+ }
180
+ continue;
181
+ }
182
+ if (expected && typeof expected === "object") {
183
+ if (!matchesOperator(actual, expected)) {
184
+ return false;
185
+ }
186
+ continue;
187
+ }
188
+ if (actual !== expected) {
189
+ return false;
190
+ }
191
+ }
192
+ return true;
193
+ };
194
+ var matchesOperator = (actual, expected) => {
195
+ if ("eq" in expected && actual !== expected.eq) {
196
+ return false;
197
+ }
198
+ if ("ne" in expected && actual === expected.ne) {
199
+ return false;
200
+ }
201
+ if ("gt" in expected && !isGreaterThan(actual, expected.gt)) {
202
+ return false;
203
+ }
204
+ if ("gte" in expected && !isGreaterThanOrEqual(actual, expected.gte)) {
205
+ return false;
206
+ }
207
+ if ("lt" in expected && !isLessThan(actual, expected.lt)) {
208
+ return false;
209
+ }
210
+ if ("lte" in expected && !isLessThanOrEqual(actual, expected.lte)) {
211
+ return false;
212
+ }
213
+ if ("in" in expected && !expected.in?.includes(actual)) {
214
+ return false;
215
+ }
216
+ if ("contains" in expected) {
217
+ return String(actual ?? "").toLowerCase().includes(String(expected.contains).toLowerCase());
218
+ }
219
+ return true;
220
+ };
221
+ var isComparable = (value) => typeof value === "number" || typeof value === "string";
222
+ var isGreaterThan = (actual, expected) => isComparable(actual) && expected !== void 0 && actual > expected;
223
+ var isGreaterThanOrEqual = (actual, expected) => isComparable(actual) && expected !== void 0 && actual >= expected;
224
+ var isLessThan = (actual, expected) => isComparable(actual) && expected !== void 0 && actual < expected;
225
+ var isLessThanOrEqual = (actual, expected) => isComparable(actual) && expected !== void 0 && actual <= expected;
226
+
227
+ export { applyQuery, assertStorageAdapter, assertSyncTransport, backoffDelay, clone, countQuery, createId, delay, findMatchingIndex, getEqualityFilterLookups, isBrowser, matchesQuery, normalizeError, now, readIndexFields, serializeCompoundIndexValue, serializeIndexValue, toQueryString, withIndexForwarding };
228
+ //# sourceMappingURL=index.js.map
229
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import {\n STORAGE_ADAPTER_CONTRACT_VERSION,\n SYNC_TRANSPORT_CONTRACT_VERSION,\n type EntityRecord,\n type IndexableStorageAdapter,\n type IndexDefinition,\n type QueryFilterValue,\n type QueryFilters,\n type QueryOptions,\n type StorageAdapter,\n type SyncTransport\n} from \"@offlinejs/types\";\n\ntype OperatorFilter = Extract<QueryFilterValue, { eq?: unknown }>;\n\nexport const now = (): number => Date.now();\n\nexport const isBrowser = (): boolean =>\n typeof globalThis.window !== \"undefined\" && typeof globalThis.document !== \"undefined\";\n\nexport const delay = (ms: number): Promise<void> =>\n new Promise((resolve) => {\n globalThis.setTimeout(resolve, ms);\n });\n\nexport const createId = (): string => {\n const cryptoApi = globalThis.crypto;\n\n if (cryptoApi?.randomUUID) {\n return cryptoApi.randomUUID();\n }\n\n return `offline_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n};\n\nexport const clone = <TValue>(value: TValue): TValue => {\n if (typeof globalThis.structuredClone === \"function\") {\n return globalThis.structuredClone(value);\n }\n\n return JSON.parse(JSON.stringify(value)) as TValue;\n};\n\nexport const normalizeError = (error: unknown): Error => {\n if (error instanceof Error) {\n return error;\n }\n\n return new Error(typeof error === \"string\" ? error : \"Unknown OfflineJS error\");\n};\n\nexport const assertStorageAdapter = (adapter: StorageAdapter): void => {\n const requiredMethods: Array<keyof StorageAdapter> = [\n \"get\",\n \"set\",\n \"delete\",\n \"find\",\n \"clear\",\n \"transaction\"\n ];\n\n if (!adapter.name) {\n throw new Error(\"OfflineJS storage adapter requires a stable name\");\n }\n\n for (const method of requiredMethods) {\n if (typeof adapter[method] !== \"function\") {\n throw new Error(`OfflineJS storage adapter \"${adapter.name}\" is missing ${String(method)}()`);\n }\n }\n\n if (\n adapter.contractVersion !== undefined &&\n adapter.contractVersion !== STORAGE_ADAPTER_CONTRACT_VERSION\n ) {\n throw new Error(\n `Unsupported storage adapter contract ${adapter.contractVersion}; expected ${STORAGE_ADAPTER_CONTRACT_VERSION}`\n );\n }\n};\n\nexport const assertSyncTransport = (transport: SyncTransport): void => {\n if (typeof transport.request !== \"function\") {\n throw new Error(\"OfflineJS sync transport requires request()\");\n }\n\n if (\n transport.contractVersion !== undefined &&\n transport.contractVersion !== SYNC_TRANSPORT_CONTRACT_VERSION\n ) {\n throw new Error(\n `Unsupported sync transport contract ${transport.contractVersion}; expected ${SYNC_TRANSPORT_CONTRACT_VERSION}`\n );\n }\n};\n\nexport const toQueryString = (\n query?: Record<string, string | number | boolean | undefined>\n): string => {\n if (!query) {\n return \"\";\n }\n\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) {\n params.set(key, String(value));\n }\n }\n\n const result = params.toString();\n return result.length > 0 ? `?${result}` : \"\";\n};\n\nexport const backoffDelay = (\n attempt: number,\n options: { baseDelayMs: number; factor: number; jitter: boolean; maxDelayMs: number }\n): number => {\n const exponentialDelay = Math.min(\n options.baseDelayMs * options.factor ** Math.max(0, attempt - 1),\n options.maxDelayMs\n );\n\n if (!options.jitter) {\n return exponentialDelay;\n }\n\n return Math.round(exponentialDelay * (0.5 + Math.random() * 0.5));\n};\n\nexport const matchesQuery = <TRecord extends EntityRecord>(\n record: TRecord,\n query: QueryOptions<TRecord> = {}\n): boolean => {\n if (query.filters && !matchesFilters(record, query.filters as Record<string, QueryFilterValue>)) {\n return false;\n }\n\n if (!query.search) {\n return true;\n }\n\n const search = query.search.toLowerCase();\n const fields = query.searchFields?.map((field) => String(field)) ?? Object.keys(record);\n\n return fields.some((field) =>\n String(record[field] ?? \"\")\n .toLowerCase()\n .includes(search)\n );\n};\n\nexport const applyQuery = <TRecord extends EntityRecord>(\n records: TRecord[],\n query: QueryOptions<TRecord> = {}\n): TRecord[] => {\n const filtered = records.filter((record) => matchesQuery(record, query));\n const sorted = sortRecords(filtered, query);\n const offset = Math.max(0, query.offset ?? 0);\n const limit = query.limit ?? sorted.length;\n\n return sorted.slice(offset, offset + limit);\n};\n\nexport const countQuery = <TRecord extends EntityRecord>(\n records: TRecord[],\n query: QueryOptions<TRecord> = {}\n): number => records.filter((record) => matchesQuery(record, query)).length;\n\nexport const serializeIndexValue = (value: unknown): string => JSON.stringify(value ?? null);\n\nexport const serializeCompoundIndexValue = (values: unknown[]): string =>\n JSON.stringify(values.map((value) => value ?? null));\n\nexport const readIndexFields = (record: EntityRecord, fields: Array<string | number | symbol>): unknown[] =>\n fields.map((field) => record[String(field)]);\n\nexport interface EqualityLookup {\n field: string;\n value: unknown;\n}\n\n/** Extract simple equality lookups that secondary indexes can accelerate. */\nexport const getEqualityFilterLookups = <TRecord extends EntityRecord>(\n filters?: QueryFilters<TRecord>\n): EqualityLookup[] => {\n if (!filters) {\n return [];\n }\n\n const lookups: EqualityLookup[] = [];\n\n for (const [field, expected] of Object.entries(filters)) {\n if (expected === undefined) {\n continue;\n }\n\n if (Array.isArray(expected) || expected === null || typeof expected !== \"object\") {\n lookups.push({ field, value: expected });\n continue;\n }\n\n if (\"eq\" in expected && expected.eq !== undefined) {\n lookups.push({ field, value: expected.eq });\n }\n }\n\n return lookups;\n};\n\nexport interface MatchedIndex {\n index: IndexDefinition;\n values: unknown[];\n}\n\n/** Prefer the most specific index whose fields are all covered by equality filters. */\nexport const findMatchingIndex = (\n indexes: IndexDefinition[],\n lookups: EqualityLookup[]\n): MatchedIndex | null => {\n if (indexes.length === 0 || lookups.length === 0) {\n return null;\n }\n\n const lookupByField = new Map(lookups.map((lookup) => [lookup.field, lookup.value]));\n const ranked = [...indexes].sort((left, right) => right.fields.length - left.fields.length);\n\n for (const index of ranked) {\n const fields = index.fields.map(String);\n\n if (fields.every((field) => lookupByField.has(field))) {\n return {\n index,\n values: fields.map((field) => lookupByField.get(field))\n };\n }\n }\n\n return null;\n};\n\n/** Forward index APIs through storage wrappers (validation/encryption). */\nexport const withIndexForwarding = <TAdapter extends StorageAdapter>(\n wrapper: TAdapter,\n inner: StorageAdapter\n): TAdapter & IndexableStorageAdapter => {\n const indexable = inner as IndexableStorageAdapter;\n\n return Object.assign(wrapper, {\n ...(typeof indexable.createIndex === \"function\"\n ? { createIndex: indexable.createIndex.bind(indexable) }\n : {}),\n ...(typeof indexable.dropIndex === \"function\"\n ? { dropIndex: indexable.dropIndex.bind(indexable) }\n : {}),\n ...(typeof indexable.listIndexes === \"function\"\n ? { listIndexes: indexable.listIndexes.bind(indexable) }\n : {})\n }) as TAdapter & IndexableStorageAdapter;\n};\n\nconst sortRecords = <TRecord extends EntityRecord>(\n records: TRecord[],\n query: QueryOptions<TRecord>\n): TRecord[] => {\n if (!query.orderBy) {\n return [...records];\n }\n\n const direction = query.sort === \"desc\" ? -1 : 1;\n const key = String(query.orderBy);\n\n return [...records].sort((left, right) => {\n const leftValue = left[key];\n const rightValue = right[key];\n\n if (leftValue === rightValue) {\n return 0;\n }\n\n if (leftValue === undefined || leftValue === null) {\n return -1 * direction;\n }\n\n if (rightValue === undefined || rightValue === null) {\n return direction;\n }\n\n return leftValue > rightValue ? direction : -direction;\n });\n};\n\nconst matchesFilters = (\n record: EntityRecord,\n filters: Record<string, QueryFilterValue>\n): boolean => {\n for (const [field, expected] of Object.entries(filters)) {\n const actual = record[field];\n\n if (Array.isArray(expected)) {\n if (!expected.includes(actual as never)) {\n return false;\n }\n continue;\n }\n\n if (expected && typeof expected === \"object\") {\n if (!matchesOperator(actual, expected)) {\n return false;\n }\n continue;\n }\n\n if (actual !== expected) {\n return false;\n }\n }\n\n return true;\n};\n\nconst matchesOperator = (actual: unknown, expected: OperatorFilter): boolean => {\n if (\"eq\" in expected && actual !== expected.eq) {\n return false;\n }\n\n if (\"ne\" in expected && actual === expected.ne) {\n return false;\n }\n\n if (\"gt\" in expected && !isGreaterThan(actual, expected.gt)) {\n return false;\n }\n\n if (\"gte\" in expected && !isGreaterThanOrEqual(actual, expected.gte)) {\n return false;\n }\n\n if (\"lt\" in expected && !isLessThan(actual, expected.lt)) {\n return false;\n }\n\n if (\"lte\" in expected && !isLessThanOrEqual(actual, expected.lte)) {\n return false;\n }\n\n if (\"in\" in expected && !expected.in?.includes(actual)) {\n return false;\n }\n\n if (\"contains\" in expected) {\n return String(actual ?? \"\")\n .toLowerCase()\n .includes(String(expected.contains).toLowerCase());\n }\n\n return true;\n};\n\nconst isComparable = (value: unknown): value is number | string =>\n typeof value === \"number\" || typeof value === \"string\";\n\nconst isGreaterThan = (actual: unknown, expected: number | string | undefined): boolean =>\n isComparable(actual) && expected !== undefined && actual > expected;\n\nconst isGreaterThanOrEqual = (actual: unknown, expected: number | string | undefined): boolean =>\n isComparable(actual) && expected !== undefined && actual >= expected;\n\nconst isLessThan = (actual: unknown, expected: number | string | undefined): boolean =>\n isComparable(actual) && expected !== undefined && actual < expected;\n\nconst isLessThanOrEqual = (actual: unknown, expected: number | string | undefined): boolean =>\n isComparable(actual) && expected !== undefined && actual <= expected;\n"]}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@offlinejs/utils",
3
+ "version": "0.1.0",
4
+ "description": "Small runtime utilities for OfflineJS packages.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "dependencies": {
17
+ "@offlinejs/types": "0.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "tsup": "latest",
21
+ "typescript": "latest"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public",
25
+ "registry": "https://registry.npmjs.org/"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "typecheck": "tsc --noEmit"
30
+ }
31
+ }