@vobs/i18n 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 vobsjs
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.
@@ -0,0 +1,107 @@
1
+ /** @license MIT
2
+ * Copyright (c) 2026 vobsjs
3
+ * @vobs/i18n
4
+ */
5
+ import { type WritableSignal } from '@vobs/reactivity';
6
+ import { type InjectionKey, type Owner } from '@vobs/runtime-core';
7
+ export type LocaleCode = string;
8
+ export type MessageValue = string;
9
+ export type MessageParams = Readonly<Record<string, string | number | boolean | null | undefined>>;
10
+ export type LocaleMessages = Readonly<Record<string, MessageValue>>;
11
+ export type LocaleMessageCatalog = Readonly<Record<LocaleCode, LocaleMessages | undefined>>;
12
+ /** DI injection key: provide(I18nKey, i18n) at app assembly; components/pages consume via useI18n (resolved along the InjectionScope parent chain). */
13
+ export declare const I18nKey: InjectionKey<I18nInstance>;
14
+ /** The injection capability subset useI18n needs (structurally compatible with ComponentSetupContext/PageSetupContext provide/maybeInject). */
15
+ export interface I18nInjectionHost {
16
+ provide<T>(key: InjectionKey<T>, value: T): () => void;
17
+ maybeInject<T>(key: InjectionKey<T>): T | undefined;
18
+ }
19
+ /**
20
+ * App assembly: provides the I18n instance into the injection chain in root component/page setup;
21
+ * child components get it via useI18n along the InjectionScope parent chain (multiple instances can coexist, isolated by assembly level).
22
+ */
23
+ export declare function provideI18n(context: I18nInjectionHost, instance: I18nInstance): () => void;
24
+ /**
25
+ * Gets the I18n instance in a component/page (composition API):
26
+ * - component setup: `const i18n = useI18n(context)`, context is ComponentSetupContext
27
+ * - page setup: `const i18n = useI18n(context)`, context is PageSetupContext
28
+ * - the app must provideI18n(context, i18n) at the root; returns undefined when not injected, the caller decides the fallback (e.g. show the raw key).
29
+ */
30
+ export declare function useI18n<Schema extends LocaleCatalogSchema = LocaleCatalogSchema>(context: I18nInjectionHost): I18nInstance<Schema> | undefined;
31
+ /** Nested message schema: values are either strings (leaves) or nested objects (`menu.file.save` dotted paths). */
32
+ export interface LocaleMessageSchema {
33
+ readonly [key: string]: MessageValue | LocaleMessageSchema;
34
+ }
35
+ /** Nested schema organized by locale (for defineMessages/createI18n generic inference). */
36
+ export type LocaleCatalogSchema = Readonly<Record<LocaleCode, LocaleMessageSchema | undefined>>;
37
+ /** Decrements recursion depth (P0-C: prevents deep schemas from triggering TS combinatorial explosion/depth limits). Any N returns a decreased value; recursion stops at zero. */
38
+ type DecrementDepth<N extends number> = N extends 0 | 1 ? 0 : N extends 2 ? 1 : N extends 3 ? 2 : N extends 4 ? 3 : N extends 5 ? 4 : N extends 6 ? 5 : N extends 7 ? 6 : 7;
39
+ /**
40
+ * Derives all leaf keys of a nested schema (dotted-path union): `{ menu: { file: { save } }, hello }` → `'menu.file.save' | 'hello'`.
41
+ * `Depth` (default 5) caps recursion; subtrees degrade to `string` at zero — deep schemas do not hit TS depth limits/union
42
+ * explosion, and top-level type safety is unaffected (P0-C).
43
+ */
44
+ export type MessageKey<Schema extends LocaleMessageSchema, Depth extends number = 5> = {
45
+ readonly [Key in keyof Schema & string]: Schema[Key] extends MessageValue ? Key : Schema[Key] extends LocaleMessageSchema ? Depth extends 0 ? string : `${Key}.${MessageKey<Schema[Key], DecrementDepth<Depth>>}` : never;
46
+ }[keyof Schema & string];
47
+ /** Derives the message key union from a catalog schema (takes the first locale's schema). Depth is passed through to MessageKey (P0-C). */
48
+ export type CatalogMessageKey<Schema extends LocaleCatalogSchema, Depth extends number = 5> = Schema[keyof Schema] extends LocaleMessageSchema ? MessageKey<Schema[keyof Schema], Depth> : string;
49
+ /** Declares a message schema type-safely: keeps the nested structure for createI18n's key union inference. */
50
+ export declare function defineMessages<const Schema extends LocaleMessageSchema>(messages: Schema): Schema;
51
+ /** Missing level: 'fallback' = missing in the active locale but the fallback hit (translation fell back); 'all' = missing from every catalog. */
52
+ export type MissingMessageLevel = 'fallback' | 'all';
53
+ /** Diagnostic data for the missing handler: distinguishes missing levels to locate catalog configuration issues. */
54
+ export interface MissingMessageInfo {
55
+ /** Missing level (see MissingMessageLevel). */
56
+ readonly level: MissingMessageLevel;
57
+ /** Configured fallback locale (undefined when not configured). */
58
+ readonly fallbackLocale?: LocaleCode;
59
+ }
60
+ export interface I18nOptions {
61
+ readonly locale: LocaleCode;
62
+ readonly fallbackLocale?: LocaleCode;
63
+ /**
64
+ * Message catalog: flat or nested (dotted paths); nested catalogs are flattened at creation.
65
+ * Broadly typed (LocaleCatalogSchema values may nest) — type safety comes from explicit `createI18n<typeof messages>`.
66
+ */
67
+ readonly messages?: LocaleCatalogSchema;
68
+ /**
69
+ * Missing-key handler (default console.warn + return the key).
70
+ * The third arg info.level distinguishes: 'fallback' (missing in the active locale, translation fell back, return value ignored),
71
+ * 'all' (missing from every catalog, return value is used as display text).
72
+ */
73
+ readonly missing?: (key: string, locale: LocaleCode, info?: MissingMessageInfo) => string;
74
+ /** Owner the I18n instance belongs to (default currentOwner); the instance is released on Owner disposal. */
75
+ readonly owner?: Owner;
76
+ }
77
+ export interface I18nInstance<Schema extends LocaleCatalogSchema = LocaleCatalogSchema> {
78
+ readonly locale: WritableSignal<LocaleCode>;
79
+ readonly fallbackLocale: LocaleCode | undefined;
80
+ /** Flattened catalog (dotted-path keys). */
81
+ readonly messages: LocaleMessageCatalog;
82
+ /** Translate: key is constrained by the schema type (nested keys use dotted paths). */
83
+ t<Key extends CatalogMessageKey<Schema>>(key: Key, params?: MessageParams): string;
84
+ setLocale(locale: LocaleCode): void;
85
+ addMessages(locale: LocaleCode, messages: LocaleMessageSchema): void;
86
+ hasMessage(key: string, locale?: LocaleCode): boolean;
87
+ formatDate(value: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
88
+ formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
89
+ formatCurrency(value: number, currency: string, options?: Intl.NumberFormatOptions): string;
90
+ formatRelativeTime(value: number, unit: Intl.RelativeTimeFormatUnit): string;
91
+ /** Releases the instance (idempotent). Called automatically on Owner disposal. */
92
+ dispose(): void;
93
+ }
94
+ export interface SerializedI18nState {
95
+ readonly locale: LocaleCode;
96
+ readonly fallbackLocale?: LocaleCode;
97
+ readonly messages: LocaleMessageCatalog;
98
+ }
99
+ export declare function createI18n<const Schema extends LocaleCatalogSchema = LocaleCatalogSchema>(options: I18nOptions): I18nInstance<Schema>;
100
+ export declare function serializeI18nState(instance: I18nInstance): SerializedI18nState;
101
+ /**
102
+ * Symmetrically rebuilds an I18n instance from serialized state (contract §4):
103
+ * serialize and hydrate are a pair — SSR serializes, and before client hydration this rebuilds,
104
+ * keeping first-screen translations identical to the SSR output (see the Security contract's SSR state boundary).
105
+ */
106
+ export declare function createI18nFromState(state: SerializedI18nState, options?: Pick<I18nOptions, 'missing' | 'owner'>): I18nInstance;
107
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,339 @@
1
+ /** @license MIT
2
+ * Copyright (c) 2026 vobsjs
3
+ * @vobs/i18n
4
+ */
5
+ import { signal } from '@vobs/reactivity';
6
+ import { createInjectionKey, currentOwner, TranslationAdapterKey, } from '@vobs/runtime-core';
7
+ /** DI injection key: provide(I18nKey, i18n) at app assembly; components/pages consume via useI18n (resolved along the InjectionScope parent chain). */
8
+ export const I18nKey = createInjectionKey('vobs.i18n');
9
+ /**
10
+ * App assembly: provides the I18n instance into the injection chain in root component/page setup;
11
+ * child components get it via useI18n along the InjectionScope parent chain (multiple instances can coexist, isolated by assembly level).
12
+ */
13
+ export function provideI18n(context, instance) {
14
+ const undoI18n = context.provide(I18nKey, instance);
15
+ // Register the TranslationAdapter port: template {{@key}} / :attr="@key" is translated through it (overridable by a user-defined adapter)
16
+ const undoAdapter = context.provide(TranslationAdapterKey, {
17
+ t: (key, params) => instance.t(key, params),
18
+ locale: instance.locale,
19
+ });
20
+ return () => {
21
+ undoI18n();
22
+ undoAdapter();
23
+ };
24
+ }
25
+ /**
26
+ * Gets the I18n instance in a component/page (composition API):
27
+ * - component setup: `const i18n = useI18n(context)`, context is ComponentSetupContext
28
+ * - page setup: `const i18n = useI18n(context)`, context is PageSetupContext
29
+ * - the app must provideI18n(context, i18n) at the root; returns undefined when not injected, the caller decides the fallback (e.g. show the raw key).
30
+ */
31
+ export function useI18n(context) {
32
+ return context.maybeInject(I18nKey);
33
+ }
34
+ /** Declares a message schema type-safely: keeps the nested structure for createI18n's key union inference. */
35
+ export function defineMessages(messages) {
36
+ return messages;
37
+ }
38
+ /** Flattens a nested schema into a dotted-path flat map (internal storage/lookup shape). */
39
+ function flattenMessages(schema, prefix = '') {
40
+ const flattened = {};
41
+ for (const [key, value] of Object.entries(schema)) {
42
+ const path = prefix === '' ? key : `${prefix}.${key}`;
43
+ if (typeof value === 'string') {
44
+ flattened[path] = value;
45
+ }
46
+ else {
47
+ Object.assign(flattened, flattenMessages(value, path));
48
+ }
49
+ }
50
+ return flattened;
51
+ }
52
+ export function createI18n(options) {
53
+ const locale = signal(options.locale);
54
+ const catalogVersion = signal(0);
55
+ const catalogs = {};
56
+ for (const [messageLocale, catalog] of Object.entries(options.messages ?? {})) {
57
+ if (catalog !== undefined && catalog !== null && typeof catalog === 'object') {
58
+ catalogs[messageLocale] = flattenMessages(catalog);
59
+ }
60
+ }
61
+ const missing = options.missing ??
62
+ ((key, locale, info) => {
63
+ if (typeof console !== 'undefined') {
64
+ if (info?.level === 'fallback') {
65
+ console.warn(`[vobs i18n] Missing message: "${key}" in locale "${locale}" (fallback "${info.fallbackLocale ?? ''}")`);
66
+ }
67
+ else {
68
+ console.warn(`[vobs i18n] Missing message: "${key}"`);
69
+ }
70
+ }
71
+ return key;
72
+ });
73
+ const resolveMessage = (key, activeLocale, params, depth) => {
74
+ if (depth > 8)
75
+ return key;
76
+ const activeMessage = catalogs[activeLocale]?.[key];
77
+ const fallbackMessage = options.fallbackLocale === undefined ? undefined : catalogs[options.fallbackLocale]?.[key];
78
+ if (activeMessage === undefined && fallbackMessage === undefined) {
79
+ // missing from every catalog: the missing return value is used as display text.
80
+ return missing(key, activeLocale, {
81
+ level: 'all',
82
+ ...(options.fallbackLocale === undefined ? {} : { fallbackLocale: options.fallbackLocale }),
83
+ });
84
+ }
85
+ const message = activeMessage ?? fallbackMessage;
86
+ if (message === undefined)
87
+ return key;
88
+ if (activeMessage === undefined) {
89
+ // missing in the active locale but the fallback hit: diagnostics distinguish the level; translation still uses the fallback text (the missing return value is ignored).
90
+ void missing(key, activeLocale, {
91
+ level: 'fallback',
92
+ ...(options.fallbackLocale === undefined ? {} : { fallbackLocale: options.fallbackLocale }),
93
+ });
94
+ }
95
+ return formatMessage(message, activeLocale, params, (nestedKey) => resolveMessage(nestedKey, activeLocale, params, depth + 1));
96
+ };
97
+ let disposed = false;
98
+ const dispose = () => {
99
+ if (disposed)
100
+ return;
101
+ disposed = true;
102
+ };
103
+ const owner = options.owner ?? currentOwner();
104
+ if (owner !== undefined) {
105
+ owner.own(dispose);
106
+ }
107
+ const instance = {
108
+ locale,
109
+ fallbackLocale: options.fallbackLocale,
110
+ get messages() {
111
+ return catalogs;
112
+ },
113
+ t(key, params) {
114
+ void catalogVersion.value;
115
+ return resolveMessage(key, locale.value, params, 0);
116
+ },
117
+ setLocale(nextLocale) {
118
+ locale.value = nextLocale;
119
+ },
120
+ addMessages(messageLocale, nextMessages) {
121
+ catalogs[messageLocale] = {
122
+ ...(catalogs[messageLocale] ?? {}),
123
+ ...flattenMessages(nextMessages),
124
+ };
125
+ catalogVersion.value += 1;
126
+ },
127
+ hasMessage(key, messageLocale = locale.value) {
128
+ return catalogs[messageLocale]?.[key] !== undefined;
129
+ },
130
+ formatDate(value, formatOptions) {
131
+ return cachedDateTimeFormat(locale.value, formatOptions).format(new Date(value));
132
+ },
133
+ formatNumber(value, formatOptions) {
134
+ return cachedNumberFormat(locale.value, formatOptions).format(value);
135
+ },
136
+ formatCurrency(value, currency, formatOptions) {
137
+ return cachedNumberFormat(locale.value, {
138
+ style: 'currency',
139
+ currency,
140
+ ...formatOptions,
141
+ }).format(value);
142
+ },
143
+ formatRelativeTime(value, unit) {
144
+ return cachedRelativeTimeFormat(locale.value).format(value, unit);
145
+ },
146
+ dispose,
147
+ };
148
+ return instance;
149
+ }
150
+ export function serializeI18nState(instance) {
151
+ return {
152
+ locale: instance.locale.value,
153
+ ...(instance.fallbackLocale === undefined ? {} : { fallbackLocale: instance.fallbackLocale }),
154
+ messages: instance.messages,
155
+ };
156
+ }
157
+ /**
158
+ * Symmetrically rebuilds an I18n instance from serialized state (contract §4):
159
+ * serialize and hydrate are a pair — SSR serializes, and before client hydration this rebuilds,
160
+ * keeping first-screen translations identical to the SSR output (see the Security contract's SSR state boundary).
161
+ */
162
+ export function createI18nFromState(state, options = {}) {
163
+ return createI18n({
164
+ locale: state.locale,
165
+ ...(state.fallbackLocale === undefined ? {} : { fallbackLocale: state.fallbackLocale }),
166
+ messages: state.messages,
167
+ ...(options.missing === undefined ? {} : { missing: options.missing }),
168
+ ...(options.owner === undefined ? {} : { owner: options.owner }),
169
+ });
170
+ }
171
+ function formatMessage(message, locale, params, resolveKey) {
172
+ if (!message.includes('{'))
173
+ return message;
174
+ let result = '';
175
+ let cursor = 0;
176
+ while (cursor < message.length) {
177
+ const open = message.indexOf('{', cursor);
178
+ if (open < 0) {
179
+ result += message.slice(cursor);
180
+ break;
181
+ }
182
+ result += message.slice(cursor, open);
183
+ const close = findClosingBrace(message, open);
184
+ if (close < 0) {
185
+ result += message.slice(open);
186
+ break;
187
+ }
188
+ result += formatPlaceholder(message.slice(open + 1, close), locale, params, resolveKey);
189
+ cursor = close + 1;
190
+ }
191
+ return result;
192
+ }
193
+ function formatPlaceholder(source, locale, params, resolveKey) {
194
+ const content = source.trim();
195
+ if (content === '')
196
+ return '{}';
197
+ if (content.startsWith('@')) {
198
+ return resolveKey(content.slice(1).trim());
199
+ }
200
+ const parts = splitTopLevel(content, ',');
201
+ const name = parts[0]?.trim() ?? '';
202
+ const type = parts[1]?.trim();
203
+ if (type === 'plural') {
204
+ return formatPlural(name, parts.slice(2).join(','), locale, params, resolveKey);
205
+ }
206
+ if (type === 'select') {
207
+ return formatSelect(name, parts.slice(2).join(','), locale, params, resolveKey);
208
+ }
209
+ if (name === '')
210
+ return `{${content}}`;
211
+ const value = params?.[name];
212
+ return value === null || value === undefined ? `{${name}}` : String(value);
213
+ }
214
+ function findClosingBrace(message, open) {
215
+ let depth = 0;
216
+ for (let index = open; index < message.length; index += 1) {
217
+ const char = message[index];
218
+ if (char === '{')
219
+ depth += 1;
220
+ else if (char === '}') {
221
+ depth -= 1;
222
+ if (depth === 0)
223
+ return index;
224
+ }
225
+ }
226
+ return -1;
227
+ }
228
+ function splitTopLevel(source, separator) {
229
+ const parts = [];
230
+ let depth = 0;
231
+ let current = '';
232
+ for (const char of source) {
233
+ if (char === '{') {
234
+ depth += 1;
235
+ current += char;
236
+ }
237
+ else if (char === '}') {
238
+ depth -= 1;
239
+ current += char;
240
+ }
241
+ else if (char === separator && depth === 0) {
242
+ parts.push(current);
243
+ current = '';
244
+ }
245
+ else {
246
+ current += char;
247
+ }
248
+ }
249
+ parts.push(current);
250
+ return parts;
251
+ }
252
+ function parseCases(body) {
253
+ const cases = [];
254
+ let index = 0;
255
+ while (index < body.length) {
256
+ while (index < body.length && /\s/u.test(body[index] ?? ''))
257
+ index += 1;
258
+ if (index >= body.length)
259
+ break;
260
+ const open = body.indexOf('{', index);
261
+ if (open < 0)
262
+ break;
263
+ const match = body.slice(index, open).trim();
264
+ const close = findClosingBrace(body, open);
265
+ if (close < 0)
266
+ break;
267
+ cases.push({ match, content: body.slice(open + 1, close) });
268
+ index = close + 1;
269
+ }
270
+ return cases;
271
+ }
272
+ function formatPlural(name, body, locale, params, resolveKey) {
273
+ const cases = parseCases(body);
274
+ const value = params?.[name];
275
+ if (typeof value === 'number') {
276
+ const category = cachedPluralRules(locale).select(value);
277
+ const exact = cases.find((entry) => entry.match === `=${value}`);
278
+ if (exact !== undefined)
279
+ return formatMessage(exact.content, locale, params, resolveKey);
280
+ const matched = cases.find((entry) => entry.match === category);
281
+ if (matched !== undefined)
282
+ return formatMessage(matched.content, locale, params, resolveKey);
283
+ }
284
+ const other = cases.find((entry) => entry.match === 'other');
285
+ return other === undefined ? '' : formatMessage(other.content, locale, params, resolveKey);
286
+ }
287
+ function formatSelect(name, body, locale, params, resolveKey) {
288
+ const cases = parseCases(body);
289
+ const value = params?.[name];
290
+ const stringValue = value === null || value === undefined ? undefined : String(value);
291
+ if (stringValue !== undefined) {
292
+ const matched = cases.find((entry) => entry.match === stringValue);
293
+ if (matched !== undefined)
294
+ return formatMessage(matched.content, locale, params, resolveKey);
295
+ }
296
+ const other = cases.find((entry) => entry.match === 'other');
297
+ return other === undefined ? '' : formatMessage(other.content, locale, params, resolveKey);
298
+ }
299
+ // Intl constructor cache: PluralRules/NumberFormat/DateTimeFormat/RelativeTimeFormat are expensive to construct;
300
+ // instances for the same locale are reusable (allowed by the Intl spec, results unchanged). Module-level caches are
301
+ // shared across instances and grow with the number of locales (bounded).
302
+ const pluralRulesCache = new Map();
303
+ const numberFormatCache = new Map();
304
+ const dateTimeFormatCache = new Map();
305
+ const relativeTimeFormatCache = new Map();
306
+ function cachedPluralRules(locale) {
307
+ let rules = pluralRulesCache.get(locale);
308
+ if (rules === undefined) {
309
+ rules = new Intl.PluralRules(locale);
310
+ pluralRulesCache.set(locale, rules);
311
+ }
312
+ return rules;
313
+ }
314
+ function cachedNumberFormat(locale, options) {
315
+ const key = options === undefined ? locale : `${locale}\u0000${JSON.stringify(options)}`;
316
+ let format = numberFormatCache.get(key);
317
+ if (format === undefined) {
318
+ format = new Intl.NumberFormat(locale, options);
319
+ numberFormatCache.set(key, format);
320
+ }
321
+ return format;
322
+ }
323
+ function cachedDateTimeFormat(locale, options) {
324
+ const key = options === undefined ? locale : `${locale}\u0000${JSON.stringify(options)}`;
325
+ let format = dateTimeFormatCache.get(key);
326
+ if (format === undefined) {
327
+ format = new Intl.DateTimeFormat(locale, options);
328
+ dateTimeFormatCache.set(key, format);
329
+ }
330
+ return format;
331
+ }
332
+ function cachedRelativeTimeFormat(locale) {
333
+ let format = relativeTimeFormatCache.get(locale);
334
+ if (format === undefined) {
335
+ format = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
336
+ relativeTimeFormatCache.set(locale, format);
337
+ }
338
+ return format;
339
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@vobs/i18n",
3
+ "version": "0.1.0",
4
+ "description": "Instance-based translation and Intl formatting primitives for vobs.",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "license": "MIT",
10
+ "author": "vobsjs",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/vobsjs/vobs.git",
14
+ "directory": "packages/features/i18n"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/vobsjs/vobs/issues"
18
+ },
19
+ "homepage": "https://github.com/vobsjs/vobs#readme",
20
+ "dependencies": {
21
+ "@vobs/reactivity": "0.1.0",
22
+ "@vobs/runtime-core": "0.1.0"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "types": "./dist/index.d.ts",
35
+ "module": "./dist/index.js",
36
+ "main": "./dist/index.js",
37
+ "sideEffects": false,
38
+ "engines": {
39
+ "node": ">=20.19.0"
40
+ }
41
+ }