@inlang/sdk 0.34.8 → 0.34.10

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.
Files changed (55) hide show
  1. package/dist/adapter/solidAdapter.js +1 -1
  2. package/dist/adapter/solidAdapter.test.js +60 -23
  3. package/dist/api.d.ts +16 -8
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/createMessageLintReportsQuery.d.ts +5 -1
  6. package/dist/createMessageLintReportsQuery.d.ts.map +1 -1
  7. package/dist/createMessageLintReportsQuery.js +165 -62
  8. package/dist/createMessagesQuery.d.ts.map +1 -1
  9. package/dist/createMessagesQuery.js +30 -12
  10. package/dist/createNewProject.d.ts +0 -5
  11. package/dist/createNewProject.d.ts.map +1 -1
  12. package/dist/createNewProject.js +0 -5
  13. package/dist/lint/message/lintSingleMessage.d.ts.map +1 -1
  14. package/dist/lint/message/lintSingleMessage.js +3 -1
  15. package/dist/loadProject.d.ts.map +1 -1
  16. package/dist/loadProject.js +6 -2
  17. package/dist/loadProject.test.js +38 -25
  18. package/dist/persistence/filelock/acquireFileLock.d.ts.map +1 -1
  19. package/dist/persistence/filelock/acquireFileLock.js +2 -2
  20. package/dist/persistence/filelock/releaseLock.js +1 -1
  21. package/dist/reactivity/solid.d.ts +2 -1
  22. package/dist/reactivity/solid.d.ts.map +1 -1
  23. package/dist/reactivity/solid.js +3 -2
  24. package/dist/reactivity/solid.test.js +38 -1
  25. package/dist/v2/createMessageBundle.d.ts +25 -0
  26. package/dist/v2/createMessageBundle.d.ts.map +1 -0
  27. package/dist/v2/createMessageBundle.js +36 -0
  28. package/dist/v2/createMessageBundle.test.d.ts +2 -0
  29. package/dist/v2/createMessageBundle.test.d.ts.map +1 -0
  30. package/dist/v2/createMessageBundle.test.js +92 -0
  31. package/dist/v2/mocks/plural/bundle.d.ts +3 -0
  32. package/dist/v2/mocks/plural/bundle.d.ts.map +1 -0
  33. package/dist/v2/mocks/plural/bundle.js +140 -0
  34. package/dist/v2/mocks/plural/bundle.test.d.ts +2 -0
  35. package/dist/v2/mocks/plural/bundle.test.d.ts.map +1 -0
  36. package/dist/v2/mocks/plural/bundle.test.js +15 -0
  37. package/package.json +3 -3
  38. package/src/adapter/solidAdapter.test.ts +78 -33
  39. package/src/adapter/solidAdapter.ts +1 -1
  40. package/src/api.ts +15 -8
  41. package/src/createMessageLintReportsQuery.ts +190 -67
  42. package/src/createMessagesQuery.ts +33 -12
  43. package/src/createNewProject.ts +0 -5
  44. package/src/createNodeishFsWithWatcher.ts +1 -1
  45. package/src/lint/message/lintSingleMessage.ts +4 -1
  46. package/src/loadProject.test.ts +45 -24
  47. package/src/loadProject.ts +7 -2
  48. package/src/persistence/filelock/acquireFileLock.ts +4 -2
  49. package/src/persistence/filelock/releaseLock.ts +1 -1
  50. package/src/reactivity/solid.test.ts +54 -1
  51. package/src/reactivity/solid.ts +3 -0
  52. package/src/v2/createMessageBundle.test.ts +95 -0
  53. package/src/v2/createMessageBundle.ts +43 -0
  54. package/src/v2/mocks/plural/bundle.test.ts +18 -0
  55. package/src/v2/mocks/plural/bundle.ts +142 -0
@@ -1,13 +1,12 @@
1
1
  import type { Message } from "@inlang/message"
2
2
  import { ReactiveMap } from "./reactivity/map.js"
3
- import { createEffect } from "./reactivity/solid.js"
3
+ import { createEffect, onCleanup } from "./reactivity/solid.js"
4
4
  import { createSubscribable } from "./loadProject.js"
5
5
  import type { InlangProject, MessageQueryApi, MessageQueryDelegate } from "./api.js"
6
6
  import type { ResolvedPluginApi } from "./resolve-modules/plugins/types.js"
7
7
  import type { resolveModules } from "./resolve-modules/resolveModules.js"
8
8
  import { createNodeishFsWithWatcher } from "./createNodeishFsWithWatcher.js"
9
9
  import type { NodeishFilesystem } from "@lix-js/fs"
10
- import { onCleanup } from "solid-js"
11
10
  import { stringifyMessage } from "./storage/helper.js"
12
11
  import { acquireFileLock } from "./persistence/filelock/acquireFileLock.js"
13
12
  import _debug from "debug"
@@ -15,7 +14,7 @@ import type { ProjectSettings } from "@inlang/project-settings"
15
14
  import { releaseLock } from "./persistence/filelock/releaseLock.js"
16
15
  import { PluginLoadMessagesError, PluginSaveMessagesError } from "./errors.js"
17
16
  import { humanIdHash } from "./storage/human-id/human-readable-id.js"
18
- const debug = _debug("sdk:createMessagesQuery")
17
+ const debug = _debug("sdk:messages")
19
18
 
20
19
  function sleep(ms: number) {
21
20
  return new Promise((resolve) => setTimeout(resolve, ms))
@@ -62,14 +61,18 @@ export function createMessagesQuery({
62
61
  }: createMessagesQueryParameters): InlangProject["query"]["messages"] {
63
62
  // @ts-expect-error
64
63
  const index = new ReactiveMap<string, Message>()
64
+ let loaded = false
65
65
 
66
66
  // filepath for the lock folder
67
67
  const messageLockDirPath = projectPath + "/messagelock"
68
68
 
69
69
  let delegate: MessageQueryDelegate | undefined = undefined
70
70
 
71
- const setDelegate = (newDelegate: MessageQueryDelegate) => {
71
+ const setDelegate = (newDelegate: MessageQueryDelegate | undefined, onLoad: boolean) => {
72
72
  delegate = newDelegate
73
+ if (newDelegate && loaded && onLoad) {
74
+ newDelegate.onLoaded([...index.values()] as Message[])
75
+ }
73
76
  }
74
77
 
75
78
  // Map default alias to message
@@ -94,6 +97,7 @@ export function createMessagesQuery({
94
97
  // we clear the index independent from the change for
95
98
  index.clear()
96
99
  defaultAliasIndex.clear()
100
+ loaded = false
97
101
 
98
102
  // Load messages -> use settings to subscribe to signals from the settings
99
103
  const _settings = settings()
@@ -143,7 +147,7 @@ export function createMessagesQuery({
143
147
  messageLockDirPath,
144
148
  messageStates,
145
149
  index,
146
- delegate,
150
+ undefined /* delegate - we don't pass it here since we will call onLoaded instead */,
147
151
  _settings, // NOTE we bang here - we don't expect the settings to become null during the livetime of a project
148
152
  resolvedPluginApi
149
153
  )
@@ -154,6 +158,7 @@ export function createMessagesQuery({
154
158
  .then(() => {
155
159
  onInitialMessageLoadResult()
156
160
  delegate?.onLoaded([...index.values()])
161
+ loaded = true
157
162
  })
158
163
  })
159
164
 
@@ -203,7 +208,7 @@ export function createMessagesQuery({
203
208
  }
204
209
 
205
210
  messageStates.messageDirtyFlags[data.id] = true
206
- delegate?.onMessageCreate(data.id, index.get(data.id))
211
+ delegate?.onMessageCreate(data.id, index.get(data.id), [...index.values()])
207
212
  scheduleSave()
208
213
  return true
209
214
  },
@@ -230,7 +235,7 @@ export function createMessagesQuery({
230
235
  if (message === undefined) return false
231
236
  index.set(where.id, { ...message, ...data })
232
237
  messageStates.messageDirtyFlags[where.id] = true
233
- delegate?.onMessageCreate(where.id, index.get(data.id))
238
+ delegate?.onMessageUpdate(where.id, index.get(data.id), [...index.values()])
234
239
  scheduleSave()
235
240
  return true
236
241
  },
@@ -242,11 +247,11 @@ export function createMessagesQuery({
242
247
  defaultAliasIndex.set(data.alias.default, data)
243
248
  }
244
249
  messageStates.messageDirtyFlags[where.id] = true
245
- delegate?.onMessageCreate(data.id, index.get(data.id))
250
+ delegate?.onMessageCreate(data.id, index.get(data.id), [...index.values()])
246
251
  } else {
247
252
  index.set(where.id, { ...message, ...data })
248
253
  messageStates.messageDirtyFlags[where.id] = true
249
- delegate?.onMessageUpdate(data.id, index.get(data.id))
254
+ delegate?.onMessageUpdate(data.id, index.get(data.id), [...index.values()])
250
255
  }
251
256
  scheduleSave()
252
257
  return true
@@ -259,7 +264,7 @@ export function createMessagesQuery({
259
264
  }
260
265
  index.delete(where.id)
261
266
  messageStates.messageDirtyFlags[where.id] = true
262
- delegate?.onMessageDelete(where.id)
267
+ delegate?.onMessageDelete(where.id, [...index.values()])
263
268
  scheduleSave()
264
269
  return true
265
270
  },
@@ -323,6 +328,8 @@ async function loadMessagesViaPlugin(
323
328
 
324
329
  let loadedMessageCount = 0
325
330
 
331
+ const deletedMessages = new Set(messages.keys())
332
+
326
333
  for (const loadedMessage of loadedMessages) {
327
334
  const loadedMessageClone = structuredClone(loadedMessage)
328
335
 
@@ -338,6 +345,8 @@ async function loadMessagesViaPlugin(
338
345
  // - this could be the case if one edits the aliase manualy
339
346
  throw new Error("more than one message with the same id or alias found ")
340
347
  } else if (currentMessages.length === 1) {
348
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- length has checked beforhand
349
+ deletedMessages.delete(currentMessages[0]!.id)
341
350
  // update message in place - leave message id and alias untouched
342
351
  loadedMessageClone.alias = {} as any
343
352
 
@@ -364,7 +373,7 @@ async function loadMessagesViaPlugin(
364
373
  messages.set(loadedMessageClone.id, loadedMessageClone)
365
374
  // NOTE could use hash instead of the whole object JSON to save memory...
366
375
  messageState.messageLoadHash[loadedMessageClone.id] = importedEnecoded
367
- delegate?.onMessageUpdate(loadedMessageClone.id, loadedMessageClone)
376
+ delegate?.onMessageUpdate(loadedMessageClone.id, loadedMessageClone, [...messages.values()])
368
377
  loadedMessageCount++
369
378
  } else {
370
379
  // message with the given alias does not exist so far
@@ -392,7 +401,7 @@ async function loadMessagesViaPlugin(
392
401
  // we don't have to check - done before hand if (messages.has(loadedMessageClone.id)) return false
393
402
  messages.set(loadedMessageClone.id, loadedMessageClone)
394
403
  messageState.messageLoadHash[loadedMessageClone.id] = importedEnecoded
395
- delegate?.onMessageUpdate(loadedMessageClone.id, loadedMessageClone)
404
+ delegate?.onMessageUpdate(loadedMessageClone.id, loadedMessageClone, [...messages.values()])
396
405
  loadedMessageCount++
397
406
  }
398
407
  if (loadedMessageCount > maxMessagesPerTick) {
@@ -403,6 +412,18 @@ async function loadMessagesViaPlugin(
403
412
  loadedMessageCount = 0
404
413
  }
405
414
  }
415
+
416
+ loadedMessageCount = 0
417
+ for (const deletedMessageId of deletedMessages) {
418
+ messages.delete(deletedMessageId)
419
+ delegate?.onMessageDelete(deletedMessageId, [...messages.values()])
420
+ loadedMessageCount++
421
+ if (loadedMessageCount > maxMessagesPerTick) {
422
+ await sleep(0)
423
+ loadedMessageCount = 0
424
+ }
425
+ }
426
+
406
427
  await releaseLock(fs as NodeishFilesystem, lockDirPath, "loadMessage", lockTime)
407
428
  lockTime = undefined
408
429
 
@@ -6,11 +6,6 @@ import { defaultProjectSettings } from "./defaultProjectSettings.js"
6
6
  /**
7
7
  * Creates a new project in the given directory.
8
8
  * The directory must be an absolute path, must not exist, and must end with {name}.inlang
9
- * Uses defaultProjectSettings unless projectSettings are provided.
10
- *
11
- * @param projectPath - Absolute path to the [name].inlang directory
12
- * @param repo - An instance of a lix repo as returned by `openRepository`
13
- * @param projectSettings - Optional project settings to use for the new project.
14
9
  */
15
10
  export async function createNewProject(args: {
16
11
  projectPath: string
@@ -20,7 +20,7 @@ export const createNodeishFsWithWatcher = (args: {
20
20
  ac.abort()
21
21
  }
22
22
  // release references
23
- abortControllers = [];
23
+ abortControllers = []
24
24
  }
25
25
 
26
26
  const makeWatcher = (path: string) => {
@@ -50,5 +50,8 @@ export const lintSingleMessage = async (args: {
50
50
 
51
51
  await Promise.all(promises)
52
52
 
53
- return { data: reports, errors }
53
+ // we sort the reports by rule id to allow us to easyly compare both
54
+ const sortedReports = reports.sort((r1, r2) => r1.ruleId.localeCompare(r2.ruleId))
55
+
56
+ return { data: sortedReports, errors }
54
57
  }
@@ -635,8 +635,10 @@ describe("functionality", () => {
635
635
 
636
636
  await new Promise((resolve) => setTimeout(resolve, 510))
637
637
 
638
- expect(await project.query.messageLintReports.getAll()).toHaveLength(1)
639
- expect((await project.query.messageLintReports.getAll())?.[0]?.ruleId).toBe(_mockLintRule.id)
638
+ expect(await project.query.messageLintReports.getAll.settled()).toHaveLength(1)
639
+ expect((await project.query.messageLintReports.getAll.settled())?.[0]?.ruleId).toBe(
640
+ _mockLintRule.id
641
+ )
640
642
  expect(project.installed.messageLintRules()).toHaveLength(1)
641
643
  })
642
644
 
@@ -1014,25 +1016,6 @@ describe("functionality", () => {
1014
1016
  })
1015
1017
 
1016
1018
  describe("lint", () => {
1017
- it.todo("should throw if lint reports are not initialized yet", async () => {
1018
- const repo = await mockRepo()
1019
- const fs = repo.nodeishFs
1020
- await fs.mkdir("/user/project", { recursive: true })
1021
- await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
1022
- const project = await loadProject({
1023
- projectPath: "/user/project/project.inlang.json",
1024
- repo,
1025
- _import,
1026
- })
1027
- // TODO: test with real lint rules
1028
- try {
1029
- const r = await project.query.messageLintReports.getAll()
1030
- expect(r).toEqual(undefined)
1031
- throw new Error("Should not reach this")
1032
- } catch (e) {
1033
- expect((e as Error).message).toBe("lint not initialized yet")
1034
- }
1035
- })
1036
1019
  it("should return the message lint reports", async () => {
1037
1020
  const settings: ProjectSettings = {
1038
1021
  sourceLanguageTag: "en",
@@ -1051,7 +1034,7 @@ describe("functionality", () => {
1051
1034
  }),
1052
1035
  })
1053
1036
  // TODO: test with real lint rules
1054
- const r = await project.query.messageLintReports.getAll()
1037
+ const r = await project.query.messageLintReports.getAll.settled()
1055
1038
  expect(r).toEqual([])
1056
1039
  })
1057
1040
  })
@@ -1083,6 +1066,23 @@ describe("functionality", () => {
1083
1066
  ],
1084
1067
  }
1085
1068
 
1069
+ const newMessage = {
1070
+ id: "test2",
1071
+ selectors: [],
1072
+ variants: [
1073
+ {
1074
+ match: [],
1075
+ languageTag: "en",
1076
+ pattern: [
1077
+ {
1078
+ type: "Text",
1079
+ value: "test",
1080
+ },
1081
+ ],
1082
+ },
1083
+ ],
1084
+ }
1085
+
1086
1086
  await fs.writeFile("./messages.json", JSON.stringify(messages))
1087
1087
 
1088
1088
  const getMessages = async (customFs: NodeishFilesystemSubset) => {
@@ -1121,9 +1121,11 @@ describe("functionality", () => {
1121
1121
  })
1122
1122
 
1123
1123
  let counter = 0
1124
+ let messageCount = 0
1124
1125
 
1125
- project.query.messages.getAll.subscribe(() => {
1126
+ project.query.messages.getAll.subscribe((messages) => {
1126
1127
  counter = counter + 1
1128
+ messageCount = messages.length
1127
1129
  })
1128
1130
 
1129
1131
  // subscribe fires once
@@ -1135,6 +1137,7 @@ describe("functionality", () => {
1135
1137
 
1136
1138
  // we didn't change the message we write into message.json - shouldn't change the messages
1137
1139
  expect(counter).toBe(1)
1140
+ expect(messageCount).toBe(1)
1138
1141
 
1139
1142
  // saving the file without changing should trigger a change
1140
1143
  messages.data[0]!.variants[0]!.pattern[0]!.value = "changed"
@@ -1142,14 +1145,32 @@ describe("functionality", () => {
1142
1145
  await new Promise((resolve) => setTimeout(resolve, 200)) // file event will lock a file and be handled sequentially - give it time to pickup the change
1143
1146
 
1144
1147
  expect(counter).toBe(2)
1148
+ expect(messageCount).toBe(1)
1145
1149
 
1146
1150
  messages.data[0]!.variants[0]!.pattern[0]!.value = "changed3"
1147
1151
 
1148
- // change file
1152
+ // change file - update message
1149
1153
  await fs.writeFile("./messages.json", JSON.stringify(messages))
1150
1154
  await new Promise((resolve) => setTimeout(resolve, 200)) // file event will lock a file and be handled sequentially - give it time to pickup the change
1151
1155
 
1152
1156
  expect(counter).toBe(3)
1157
+ expect(messageCount).toBe(1)
1158
+
1159
+ // change file - add a message
1160
+ messages.data.push(newMessage)
1161
+ await fs.writeFile("./messages.json", JSON.stringify(messages))
1162
+ await new Promise((resolve) => setTimeout(resolve, 200)) // file event will lock a file and be handled sequentially - give it time to pickup the change
1163
+
1164
+ expect(counter).toBe(4)
1165
+ expect(messageCount).toBe(2)
1166
+
1167
+ // change file - remove a message
1168
+ messages.data.pop()
1169
+ await fs.writeFile("./messages.json", JSON.stringify(messages))
1170
+ await new Promise((resolve) => setTimeout(resolve, 200)) // file event will lock a file and be handled sequentially - give it time to pickup the change
1171
+
1172
+ expect(counter).toBe(5)
1173
+ expect(messageCount).toBe(1)
1153
1174
  })
1154
1175
  })
1155
1176
  })
@@ -12,7 +12,7 @@ import {
12
12
  ProjectSettingsFileNotFoundError,
13
13
  ProjectSettingsInvalidError,
14
14
  } from "./errors.js"
15
- import { createRoot, createSignal, createEffect } from "./reactivity/solid.js"
15
+ import { createRoot, createSignal, createEffect, batch } from "./reactivity/solid.js"
16
16
  import { createMessagesQuery } from "./createMessagesQuery.js"
17
17
  import { createMessageLintReportsQuery } from "./createMessageLintReportsQuery.js"
18
18
  import { ProjectSettings, type NodeishFilesystemSubset } from "./versionedInterfaces.js"
@@ -111,7 +111,12 @@ export async function loadProject(args: {
111
111
  pathPattern: projectPath + "/messages.json",
112
112
  }
113
113
  }
114
- _setSettings(validatedSettings)
114
+
115
+ batch(() => {
116
+ // reset the resolved modules first - since they are no longer valid at that point
117
+ setResolvedModules(undefined)
118
+ _setSettings(validatedSettings)
119
+ })
115
120
 
116
121
  writeSettingsToDisk(validatedSettings)
117
122
  return { data: undefined }
@@ -1,7 +1,7 @@
1
1
  import { type NodeishFilesystem } from "@lix-js/fs"
2
2
  import type { NodeishStats } from "@lix-js/fs"
3
3
  import _debug from "debug"
4
- const debug = _debug("sdk:acquireFileLock")
4
+ const debug = _debug("sdk:fileLock")
5
5
 
6
6
  const maxRetries = 10
7
7
  const nProbes = 50
@@ -13,7 +13,9 @@ export async function acquireFileLock(
13
13
  tryCount: number = 0
14
14
  ): Promise<number> {
15
15
  if (tryCount > maxRetries) {
16
- throw new Error(lockOrigin + " exceeded maximum Retries (5) to acquire lockfile " + tryCount)
16
+ throw new Error(
17
+ `${lockOrigin} exceeded maximum retries (${maxRetries}) to acquire lockfile ${tryCount}`
18
+ )
17
19
  }
18
20
 
19
21
  try {
@@ -1,6 +1,6 @@
1
1
  import { type NodeishFilesystem } from "@lix-js/fs"
2
2
  import _debug from "debug"
3
- const debug = _debug("sdk:releaseLock")
3
+ const debug = _debug("sdk:fileLock")
4
4
 
5
5
  export async function releaseLock(
6
6
  fs: NodeishFilesystem,
@@ -1,5 +1,13 @@
1
1
  import { describe, it, expect } from "vitest"
2
- import { createRoot, createSignal, createEffect, createMemo, createResource } from "./solid.js"
2
+ import {
3
+ createRoot,
4
+ createSignal,
5
+ createEffect,
6
+ createMemo,
7
+ createResource,
8
+ untrack,
9
+ } from "./solid.js"
10
+ import { ReactiveMap } from "./map.js"
3
11
 
4
12
  function sleep(ms: number) {
5
13
  return new Promise((resolve) => setTimeout(resolve, ms))
@@ -174,3 +182,48 @@ describe("solid", () => {
174
182
  expect(memo()).toBe("memo = 2000")
175
183
  })
176
184
  })
185
+
186
+ describe("solid", () => {
187
+ it("solid reactive map allows to use untrack", () => {
188
+ // @ts-expect-error -- ReactiveMap seem to have problems type arguments here
189
+ const reactiveMap = new ReactiveMap<string, any>()
190
+ const [plainSignal, setPlainSignal] = createSignal(0)
191
+
192
+ let shouldTriggerOnBoth = -1
193
+ createEffect(() => {
194
+ shouldTriggerOnBoth++
195
+ reactiveMap.values()
196
+ plainSignal()
197
+ })
198
+
199
+ let shouldTriggerOnReactiveMapOnly = -1
200
+ createEffect(() => {
201
+ shouldTriggerOnReactiveMapOnly++
202
+ reactiveMap.values()
203
+ untrack(() => plainSignal())
204
+ })
205
+
206
+ let shouldTriggerOnPlainSignalOnly = -1
207
+ createEffect(() => {
208
+ shouldTriggerOnPlainSignalOnly++
209
+ untrack(() => reactiveMap.values())
210
+ plainSignal()
211
+ })
212
+
213
+ expect(shouldTriggerOnBoth).toBe(0)
214
+ expect(shouldTriggerOnReactiveMapOnly).toBe(0)
215
+ expect(shouldTriggerOnPlainSignalOnly).toBe(0)
216
+
217
+ setPlainSignal(1)
218
+
219
+ expect(shouldTriggerOnBoth).toBe(1)
220
+ expect(shouldTriggerOnReactiveMapOnly).toBe(0)
221
+ expect(shouldTriggerOnPlainSignalOnly).toBe(1)
222
+
223
+ reactiveMap.set("a", "a")
224
+
225
+ expect(shouldTriggerOnBoth).toBe(2)
226
+ expect(shouldTriggerOnReactiveMapOnly).toBe(1)
227
+ expect(shouldTriggerOnPlainSignalOnly).toBe(1)
228
+ })
229
+ })
@@ -4,6 +4,7 @@ import {
4
4
  createRoot as _createRoot,
5
5
  createEffect as _createEffect,
6
6
  createResource as _createResource,
7
+ untrack as _untrack,
7
8
  observable as _observable,
8
9
  batch as _batch,
9
10
  from as _from,
@@ -23,6 +24,7 @@ const from = _from as typeof import("solid-js")["from"]
23
24
  const batch = _batch as typeof import("solid-js")["batch"]
24
25
  const getListener = _getListener as typeof import("solid-js")["getListener"]
25
26
  const onCleanup = _onCleanup as typeof import("solid-js")["onCleanup"]
27
+ const untrack = _untrack as typeof import("solid-js")["untrack"]
26
28
 
27
29
  export {
28
30
  createSignal,
@@ -35,5 +37,6 @@ export {
35
37
  batch,
36
38
  getListener,
37
39
  onCleanup,
40
+ untrack,
38
41
  DEV,
39
42
  }
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { createMessageBundle, createMessage } from "./createMessageBundle.js"
3
+ import { MessageBundle } from "./types.js"
4
+ import { Value } from "@sinclair/typebox/value"
5
+
6
+ describe("createMessageBundle", () => {
7
+ it("creates a bundle with no messages", () => {
8
+ const bundle: unknown = createMessageBundle({ id: "no_messages", messages: [] })
9
+ expect(Value.Check(MessageBundle, bundle)).toBe(true)
10
+ expect(bundle).toEqual({
11
+ id: "no_messages",
12
+ alias: {},
13
+ messages: [],
14
+ } satisfies MessageBundle)
15
+ })
16
+
17
+ it("creates a bundle with a single text-only message", () => {
18
+ const bundle: unknown = createMessageBundle({
19
+ id: "hello_world",
20
+ messages: [createMessage({ locale: "en", text: "Hello World!" })],
21
+ })
22
+ expect(Value.Check(MessageBundle, bundle)).toBe(true)
23
+ expect(bundle).toEqual({
24
+ id: "hello_world",
25
+ alias: {},
26
+ messages: [
27
+ {
28
+ locale: "en",
29
+ declarations: [],
30
+ selectors: [],
31
+ variants: [
32
+ {
33
+ match: [],
34
+ pattern: [
35
+ {
36
+ type: "text",
37
+ value: "Hello World!",
38
+ },
39
+ ],
40
+ },
41
+ ],
42
+ },
43
+ ],
44
+ } satisfies MessageBundle)
45
+ })
46
+
47
+ it("creates a bundle with multiple text-only messages", () => {
48
+ const bundle: unknown = createMessageBundle({
49
+ id: "hello_world_2",
50
+ messages: [
51
+ createMessage({ locale: "en", text: "Hello World!" }),
52
+ createMessage({ locale: "de", text: "Hallo Welt!" }),
53
+ ],
54
+ })
55
+ expect(Value.Check(MessageBundle, bundle)).toBe(true)
56
+ expect(bundle).toEqual({
57
+ id: "hello_world_2",
58
+ alias: {},
59
+ messages: [
60
+ {
61
+ locale: "en",
62
+ declarations: [],
63
+ selectors: [],
64
+ variants: [
65
+ {
66
+ match: [],
67
+ pattern: [
68
+ {
69
+ type: "text",
70
+ value: "Hello World!",
71
+ },
72
+ ],
73
+ },
74
+ ],
75
+ },
76
+ {
77
+ locale: "de",
78
+ declarations: [],
79
+ selectors: [],
80
+ variants: [
81
+ {
82
+ match: [],
83
+ pattern: [
84
+ {
85
+ type: "text",
86
+ value: "Hallo Welt!",
87
+ },
88
+ ],
89
+ },
90
+ ],
91
+ },
92
+ ],
93
+ } satisfies MessageBundle)
94
+ })
95
+ })
@@ -0,0 +1,43 @@
1
+ import { LanguageTag, MessageBundle, Message, Text } from "./types.js"
2
+
3
+ /**
4
+ * create v2 MessageBundle
5
+ * @example createMessageBundle({
6
+ * id: "greeting",
7
+ * messages: [
8
+ * createMessage({locale: "en", text: "Hello world!"})
9
+ * createMessage({locale: "de", text: "Hallo Welt!"})
10
+ * ]
11
+ * })
12
+ */
13
+ export function createMessageBundle(args: {
14
+ id: string
15
+ messages: Message[]
16
+ alias?: MessageBundle["alias"]
17
+ }): MessageBundle {
18
+ return {
19
+ id: args.id,
20
+ alias: args.alias ?? {},
21
+ messages: args.messages,
22
+ }
23
+ }
24
+
25
+ /**
26
+ * create v2 Messsage AST with text-only pattern
27
+ * @example createMessage({locale: "en", text: "Hello world"})
28
+ */
29
+ export function createMessage(args: { locale: LanguageTag; text: string }): Message {
30
+ return {
31
+ locale: args.locale,
32
+ declarations: [],
33
+ selectors: [],
34
+ variants: [{ match: [], pattern: [toTextElement(args.text ?? "")] }],
35
+ }
36
+ }
37
+
38
+ function toTextElement(text: string): Text {
39
+ return {
40
+ type: "text",
41
+ value: text,
42
+ }
43
+ }
@@ -0,0 +1,18 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+
3
+ import { describe, it, expect } from "vitest"
4
+ import { bundle } from "./bundle.js"
5
+ import { MessageBundle } from "../../types.js"
6
+ import { Value } from "@sinclair/typebox/value"
7
+
8
+ describe("mock plural messageBundle", () => {
9
+ it("is valid", () => {
10
+ const messageBundle: unknown = bundle
11
+ expect(Value.Check(MessageBundle, messageBundle)).toBe(true)
12
+
13
+ expect(bundle.messages.length).toBe(2)
14
+ expect(bundle.messages[0]!.declarations.length).toBe(1)
15
+ expect(bundle.messages[0]!.selectors.length).toBe(1)
16
+ expect(bundle.messages[0]!.variants.length).toBe(3)
17
+ })
18
+ })