@inlang/sdk 0.9.0 → 0.11.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.
Files changed (51) hide show
  1. package/dist/adapter/solidAdapter.test.js +12 -8
  2. package/dist/errors.d.ts +18 -5
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js +12 -10
  5. package/dist/loadProject.d.ts +4 -1
  6. package/dist/loadProject.d.ts.map +1 -1
  7. package/dist/loadProject.js +22 -11
  8. package/dist/loadProject.test.js +122 -43
  9. package/dist/resolve-modules/createNodeishFsWithAbsolutePaths.d.ts +6 -0
  10. package/dist/resolve-modules/createNodeishFsWithAbsolutePaths.d.ts.map +1 -0
  11. package/dist/resolve-modules/createNodeishFsWithAbsolutePaths.js +20 -0
  12. package/dist/resolve-modules/createNodeishFsWithAbsolutePaths.test.d.ts +2 -0
  13. package/dist/resolve-modules/createNodeishFsWithAbsolutePaths.test.d.ts.map +1 -0
  14. package/dist/resolve-modules/createNodeishFsWithAbsolutePaths.test.js +85 -0
  15. package/dist/resolve-modules/errors.d.ts +6 -5
  16. package/dist/resolve-modules/errors.d.ts.map +1 -1
  17. package/dist/resolve-modules/errors.js +10 -8
  18. package/dist/resolve-modules/import.d.ts.map +1 -1
  19. package/dist/resolve-modules/import.js +5 -5
  20. package/dist/resolve-modules/message-lint-rules/errors.d.ts +5 -4
  21. package/dist/resolve-modules/message-lint-rules/errors.d.ts.map +1 -1
  22. package/dist/resolve-modules/message-lint-rules/errors.js +2 -4
  23. package/dist/resolve-modules/message-lint-rules/resolveMessageLintRules.d.ts.map +1 -1
  24. package/dist/resolve-modules/message-lint-rules/resolveMessageLintRules.js +4 -2
  25. package/dist/resolve-modules/plugins/errors.d.ts +35 -28
  26. package/dist/resolve-modules/plugins/errors.d.ts.map +1 -1
  27. package/dist/resolve-modules/plugins/errors.js +23 -30
  28. package/dist/resolve-modules/plugins/resolvePlugins.d.ts.map +1 -1
  29. package/dist/resolve-modules/plugins/resolvePlugins.js +15 -14
  30. package/dist/resolve-modules/plugins/types.d.ts +2 -2
  31. package/dist/resolve-modules/plugins/types.d.ts.map +1 -1
  32. package/dist/resolve-modules/resolveModules.d.ts.map +1 -1
  33. package/dist/resolve-modules/resolveModules.js +5 -4
  34. package/dist/resolve-modules/resolveModules.test.js +2 -2
  35. package/dist/test-utilities/createMessage.d.ts +1 -1
  36. package/package.json +1 -1
  37. package/src/adapter/solidAdapter.test.ts +12 -8
  38. package/src/errors.ts +19 -10
  39. package/src/loadProject.test.ts +132 -43
  40. package/src/loadProject.ts +30 -18
  41. package/src/resolve-modules/createNodeishFsWithAbsolutePaths.test.ts +102 -0
  42. package/src/resolve-modules/createNodeishFsWithAbsolutePaths.ts +30 -0
  43. package/src/resolve-modules/errors.ts +14 -8
  44. package/src/resolve-modules/import.ts +5 -5
  45. package/src/resolve-modules/message-lint-rules/errors.ts +5 -5
  46. package/src/resolve-modules/message-lint-rules/resolveMessageLintRules.ts +4 -2
  47. package/src/resolve-modules/plugins/errors.ts +34 -36
  48. package/src/resolve-modules/plugins/resolvePlugins.ts +17 -46
  49. package/src/resolve-modules/plugins/types.ts +2 -2
  50. package/src/resolve-modules/resolveModules.test.ts +2 -2
  51. package/src/resolve-modules/resolveModules.ts +5 -6
@@ -88,10 +88,11 @@ const $import: ImportFunction = async (name) => ({
88
88
  describe("config", () => {
89
89
  it("should react to changes in config", async () => {
90
90
  const fs = createNodeishMemoryFs()
91
- await fs.writeFile("./project.inlang.json", JSON.stringify(config))
91
+ await fs.mkdir("/user/project", { recursive: true })
92
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(config))
92
93
  const project = solidAdapter(
93
94
  await loadProject({
94
- settingsFilePath: "./project.inlang.json",
95
+ settingsFilePath: "/user/project/project.inlang.json",
95
96
  nodeishFs: fs,
96
97
  _import: $import,
97
98
  }),
@@ -118,10 +119,11 @@ describe("config", () => {
118
119
  describe("installed", () => {
119
120
  it("react to changes that are unrelated to installed items", async () => {
120
121
  const fs = createNodeishMemoryFs()
121
- await fs.writeFile("./project.inlang.json", JSON.stringify(config))
122
+ await fs.mkdir("/user/project", { recursive: true })
123
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(config))
122
124
  const project = solidAdapter(
123
125
  await loadProject({
124
- settingsFilePath: "./project.inlang.json",
126
+ settingsFilePath: "/user/project/project.inlang.json",
125
127
  nodeishFs: fs,
126
128
  _import: $import,
127
129
  }),
@@ -181,10 +183,11 @@ describe("messages", () => {
181
183
 
182
184
  const mockImport: ImportFunction = async () => ({ default: mockPlugin })
183
185
 
184
- await fs.writeFile("./project.inlang.json", JSON.stringify(mockConfig))
186
+ await fs.mkdir("/user/project", { recursive: true })
187
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(mockConfig))
185
188
  const project = solidAdapter(
186
189
  await loadProject({
187
- settingsFilePath: "./project.inlang.json",
190
+ settingsFilePath: "/user/project/project.inlang.json",
188
191
  nodeishFs: fs,
189
192
  _import: mockImport,
190
193
  }),
@@ -210,10 +213,11 @@ describe("messages", () => {
210
213
 
211
214
  it("should react to changes in messages", async () => {
212
215
  const fs = createNodeishMemoryFs()
213
- await fs.writeFile("./project.inlang.json", JSON.stringify(config))
216
+ await fs.mkdir("/user/project", { recursive: true })
217
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(config))
214
218
  const project = solidAdapter(
215
219
  await loadProject({
216
- settingsFilePath: "./project.inlang.json",
220
+ settingsFilePath: "/user/project/project.inlang.json",
217
221
  nodeishFs: fs,
218
222
  _import: $import,
219
223
  }),
package/src/errors.ts CHANGED
@@ -1,34 +1,43 @@
1
+ import type { ValueError } from "@sinclair/typebox/errors"
2
+
1
3
  export class ProjectSettingsInvalidError extends Error {
2
- constructor(message: string, options: ErrorOptions) {
3
- super(message, options)
4
+ constructor(options: { errors: ValueError[] }) {
5
+ super(
6
+ `The project settings are invalid:\n\n${options.errors
7
+ .map((error) => `The value of "${error.path}" is invalid:\n\n${error.message}`)
8
+ .join("\n")}`
9
+ )
4
10
  this.name = "ProjectSettingsInvalidError"
5
11
  }
6
12
  }
7
13
 
8
14
  export class ProjectSettingsFileJSONSyntaxError extends Error {
9
- constructor(message: string, options: ErrorOptions) {
10
- super(message, options)
15
+ constructor(options: { cause: ErrorOptions["cause"]; path: string }) {
16
+ super(
17
+ `The settings file at "${options.path}" is not a valid JSON file:\n\n${options.cause}`,
18
+ options
19
+ )
11
20
  this.name = "ProjectSettingsFileJSONSyntaxError"
12
21
  }
13
22
  }
14
23
 
15
24
  export class ProjectSettingsFileNotFoundError extends Error {
16
- constructor(message: string, options: ErrorOptions) {
17
- super(message, options)
25
+ constructor(options: { cause?: ErrorOptions["cause"]; path: string }) {
26
+ super(`The file at "${options.path}" could not be read. Does the file exists?`, options)
18
27
  this.name = "ProjectSettingsFileNotFoundError"
19
28
  }
20
29
  }
21
30
 
22
31
  export class PluginSaveMessagesError extends Error {
23
- constructor(message: string, options: ErrorOptions) {
24
- super(message, options)
32
+ constructor(options: { cause: ErrorOptions["cause"] }) {
33
+ super(`An error occured in saveMessages() caused by ${options.cause}.`, options)
25
34
  this.name = "PluginSaveMessagesError"
26
35
  }
27
36
  }
28
37
 
29
38
  export class PluginLoadMessagesError extends Error {
30
- constructor(message: string, options: ErrorOptions) {
31
- super(message, options)
39
+ constructor(options: { cause: ErrorOptions["cause"] }) {
40
+ super(`An error occured in loadMessages() caused by ${options.cause}.`, options)
32
41
  this.name = "PluginLoadMessagesError"
33
42
  }
34
43
  }
@@ -10,6 +10,7 @@ import {
10
10
  ProjectSettingsInvalidError,
11
11
  } from "./errors.js"
12
12
  import { createNodeishMemoryFs } from "@lix-js/fs"
13
+ import { createMessage } from "./test-utilities/createMessage.js"
13
14
 
14
15
  // ------------------------------------------------------------------------------------------------
15
16
 
@@ -99,9 +100,10 @@ describe("initialization", () => {
99
100
  describe("settings", () => {
100
101
  it("should return an error if settings file is not found", async () => {
101
102
  const fs = createNodeishMemoryFs()
103
+ fs.mkdir("/user/project", { recursive: true })
102
104
 
103
105
  const project = await loadProject({
104
- settingsFilePath: "./test.json",
106
+ settingsFilePath: "/user/project/test.json",
105
107
  nodeishFs: fs,
106
108
  _import,
107
109
  })
@@ -111,10 +113,11 @@ describe("initialization", () => {
111
113
 
112
114
  it("should return an error if settings file is not a valid JSON", async () => {
113
115
  const fs = await createNodeishMemoryFs()
114
- await fs.writeFile("./project.inlang.json", "invalid json")
116
+ await fs.mkdir("/user/project", { recursive: true })
117
+ await fs.writeFile("/user/project/project.inlang.json", "invalid json")
115
118
 
116
119
  const project = await loadProject({
117
- settingsFilePath: "./project.inlang.json",
120
+ settingsFilePath: "/user/project/project.inlang.json",
118
121
  nodeishFs: fs,
119
122
  _import,
120
123
  })
@@ -124,10 +127,11 @@ describe("initialization", () => {
124
127
 
125
128
  it("should return an error if settings file is does not match schema", async () => {
126
129
  const fs = await createNodeishMemoryFs()
127
- await fs.writeFile("./project.inlang.json", JSON.stringify({}))
130
+ await fs.mkdir("/user/project", { recursive: true })
131
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify({}))
128
132
 
129
133
  const project = await loadProject({
130
- settingsFilePath: "./project.inlang.json",
134
+ settingsFilePath: "/user/project/project.inlang.json",
131
135
  nodeishFs: fs,
132
136
  _import,
133
137
  })
@@ -137,9 +141,10 @@ describe("initialization", () => {
137
141
 
138
142
  it("should return the parsed settings", async () => {
139
143
  const fs = await createNodeishMemoryFs()
140
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
144
+ await fs.mkdir("/user/project", { recursive: true })
145
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
141
146
  const project = await loadProject({
142
- settingsFilePath: "./project.inlang.json",
147
+ settingsFilePath: "/user/project/project.inlang.json",
143
148
  nodeishFs: fs,
144
149
  _import,
145
150
  })
@@ -150,22 +155,27 @@ describe("initialization", () => {
150
155
  it("should not re-write the settings to disk when initializing", async () => {
151
156
  const fs = await createNodeishMemoryFs()
152
157
  const settingsWithDeifferentFormatting = JSON.stringify(settings, undefined, 4)
153
- await fs.writeFile("./project.inlang.json", settingsWithDeifferentFormatting)
158
+ await fs.mkdir("/user/project", { recursive: true })
159
+ await fs.writeFile("/user/project/project.inlang.json", settingsWithDeifferentFormatting)
154
160
 
155
161
  const project = await loadProject({
156
- settingsFilePath: "./project.inlang.json",
162
+ settingsFilePath: "/user/project/project.inlang.json",
157
163
  nodeishFs: fs,
158
164
  _import,
159
165
  })
160
166
 
161
- const settingsOnDisk = await fs.readFile("./project.inlang.json", { encoding: "utf-8" })
167
+ const settingsOnDisk = await fs.readFile("/user/project/project.inlang.json", {
168
+ encoding: "utf-8",
169
+ })
162
170
  expect(settingsOnDisk).toBe(settingsWithDeifferentFormatting)
163
171
 
164
172
  project.setSettings(project.settings())
165
173
  // TODO: how can we await `setsettings` correctly
166
174
  await new Promise((resolve) => setTimeout(resolve, 0))
167
175
 
168
- const newsettingsOnDisk = await fs.readFile("./project.inlang.json", { encoding: "utf-8" })
176
+ const newsettingsOnDisk = await fs.readFile("/user/project/project.inlang.json", {
177
+ encoding: "utf-8",
178
+ })
169
179
  expect(newsettingsOnDisk).not.toBe(settingsWithDeifferentFormatting)
170
180
  })
171
181
  })
@@ -178,10 +188,11 @@ describe("initialization", () => {
178
188
  } satisfies InlangModule)
179
189
 
180
190
  const fs = createNodeishMemoryFs()
181
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
191
+ await fs.mkdir("/user/project", { recursive: true })
192
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
182
193
 
183
194
  const project = await loadProject({
184
- settingsFilePath: "./project.inlang.json",
195
+ settingsFilePath: "/user/project/project.inlang.json",
185
196
  nodeishFs: fs,
186
197
  _import: $badImport,
187
198
  })
@@ -210,9 +221,10 @@ describe("functionality", () => {
210
221
  describe("settings", () => {
211
222
  it("should return the settings", async () => {
212
223
  const fs = await createNodeishMemoryFs()
213
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
224
+ await fs.mkdir("/user/project", { recursive: true })
225
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
214
226
  const project = await loadProject({
215
- settingsFilePath: "./project.inlang.json",
227
+ settingsFilePath: "/user/project/project.inlang.json",
216
228
  nodeishFs: fs,
217
229
  _import,
218
230
  })
@@ -222,9 +234,10 @@ describe("functionality", () => {
222
234
 
223
235
  it("should set a new settings", async () => {
224
236
  const fs = await createNodeishMemoryFs()
225
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
237
+ await fs.mkdir("/user/project", { recursive: true })
238
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
226
239
  const project = await loadProject({
227
- settingsFilePath: "./project.inlang.json",
240
+ settingsFilePath: "/user/project/project.inlang.json",
228
241
  nodeishFs: fs,
229
242
  _import,
230
243
  })
@@ -247,9 +260,10 @@ describe("functionality", () => {
247
260
  describe("setSettings", () => {
248
261
  it("should fail if settings is not valid", async () => {
249
262
  const fs = await createNodeishMemoryFs()
250
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
263
+ await fs.mkdir("/user/project", { recursive: true })
264
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
251
265
  const project = await loadProject({
252
- settingsFilePath: "./project.inlang.json",
266
+ settingsFilePath: "/user/project/project.inlang.json",
253
267
  nodeishFs: fs,
254
268
  _import,
255
269
  })
@@ -261,14 +275,15 @@ describe("functionality", () => {
261
275
 
262
276
  it("should write settings to disk", async () => {
263
277
  const fs = await createNodeishMemoryFs()
264
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
278
+ await fs.mkdir("/user/project", { recursive: true })
279
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
265
280
  const project = await loadProject({
266
- settingsFilePath: "./project.inlang.json",
281
+ settingsFilePath: "/user/project/project.inlang.json",
267
282
  nodeishFs: fs,
268
283
  _import,
269
284
  })
270
285
 
271
- const before = await fs.readFile("./project.inlang.json", { encoding: "utf-8" })
286
+ const before = await fs.readFile("/user/project/project.inlang.json", { encoding: "utf-8" })
272
287
  expect(before).toBeDefined()
273
288
 
274
289
  const result = project.setSettings({ ...settings, languageTags: [] })
@@ -278,7 +293,7 @@ describe("functionality", () => {
278
293
  // TODO: how to wait for fs.writeFile to finish?
279
294
  await new Promise((resolve) => setTimeout(resolve, 0))
280
295
 
281
- const after = await fs.readFile("./project.inlang.json", { encoding: "utf-8" })
296
+ const after = await fs.readFile("/user/project/project.inlang.json", { encoding: "utf-8" })
282
297
  expect(after).toBeDefined()
283
298
  expect(after).not.toBe(before)
284
299
  })
@@ -292,9 +307,10 @@ describe("functionality", () => {
292
307
  languageTags: ["en"],
293
308
  modules: ["plugin.js", "lintRule.js"],
294
309
  }
295
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
310
+ await fs.mkdir("/user/project", { recursive: true })
311
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
296
312
  const project = await loadProject({
297
- settingsFilePath: "./project.inlang.json",
313
+ settingsFilePath: "/user/project/project.inlang.json",
298
314
  nodeishFs: fs,
299
315
  _import,
300
316
  })
@@ -324,10 +340,11 @@ describe("functionality", () => {
324
340
  modules: ["plugin.js", "lintRule.js"],
325
341
  }
326
342
 
327
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
343
+ await fs.mkdir("/user/project", { recursive: true })
344
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
328
345
 
329
346
  const project = await loadProject({
330
- settingsFilePath: "./project.inlang.json",
347
+ settingsFilePath: "/user/project/project.inlang.json",
331
348
  nodeishFs: fs,
332
349
  _import,
333
350
  })
@@ -357,8 +374,9 @@ describe("functionality", () => {
357
374
  saveMessages: () => undefined,
358
375
  }
359
376
  const fs = await createNodeishMemoryFs()
377
+ await fs.mkdir("/user/project", { recursive: true })
360
378
  await fs.writeFile(
361
- "./project.inlang.json",
379
+ "/user/project/project.inlang.json",
362
380
  JSON.stringify({
363
381
  sourceLanguageTag: "en",
364
382
  languageTags: ["en"],
@@ -372,7 +390,7 @@ describe("functionality", () => {
372
390
  } satisfies InlangModule
373
391
  }
374
392
  const project = await loadProject({
375
- settingsFilePath: "./project.inlang.json",
393
+ settingsFilePath: "/user/project/project.inlang.json",
376
394
  nodeishFs: fs,
377
395
  _import,
378
396
  })
@@ -405,8 +423,9 @@ describe("functionality", () => {
405
423
  saveMessages: () => undefined,
406
424
  }
407
425
  const fs = await createNodeishMemoryFs()
426
+ await fs.mkdir("/user/project", { recursive: true })
408
427
  await fs.writeFile(
409
- "./project.settings.json",
428
+ "/user/project/project.inlang.json",
410
429
  JSON.stringify({
411
430
  sourceLanguageTag: "en",
412
431
  languageTags: ["en"],
@@ -420,7 +439,7 @@ describe("functionality", () => {
420
439
  }
421
440
 
422
441
  const project = await loadProject({
423
- settingsFilePath: "./project.settings.json",
442
+ settingsFilePath: "/user/project/project.inlang.json",
424
443
  nodeishFs: fs,
425
444
  _import,
426
445
  })
@@ -436,9 +455,10 @@ describe("functionality", () => {
436
455
  describe("errors", () => {
437
456
  it("should return the errors", async () => {
438
457
  const fs = await createNodeishMemoryFs()
439
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
458
+ await fs.mkdir("/user/project", { recursive: true })
459
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
440
460
  const project = await loadProject({
441
- settingsFilePath: "./project.inlang.json",
461
+ settingsFilePath: "/user/project/project.inlang.json",
442
462
  nodeishFs: fs,
443
463
  _import,
444
464
  })
@@ -451,9 +471,10 @@ describe("functionality", () => {
451
471
  describe("customApi", () => {
452
472
  it("should return the app specific api", async () => {
453
473
  const fs = await createNodeishMemoryFs()
454
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
474
+ await fs.mkdir("/user/project", { recursive: true })
475
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
455
476
  const project = await loadProject({
456
- settingsFilePath: "./project.inlang.json",
477
+ settingsFilePath: "/user/project/project.inlang.json",
457
478
  nodeishFs: fs,
458
479
  _import,
459
480
  })
@@ -467,9 +488,10 @@ describe("functionality", () => {
467
488
  describe("messages", () => {
468
489
  it("should return the messages", async () => {
469
490
  const fs = await createNodeishMemoryFs()
470
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
491
+ await fs.mkdir("/user/project", { recursive: true })
492
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
471
493
  const project = await loadProject({
472
- settingsFilePath: "./project.inlang.json",
494
+ settingsFilePath: "/user/project/project.inlang.json",
473
495
  nodeishFs: fs,
474
496
  _import,
475
497
  })
@@ -491,7 +513,8 @@ describe("functionality", () => {
491
513
  },
492
514
  }
493
515
 
494
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
516
+ await fs.mkdir("/user/project", { recursive: true })
517
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
495
518
 
496
519
  await fs.mkdir("./resources")
497
520
 
@@ -512,7 +535,7 @@ describe("functionality", () => {
512
535
  }
513
536
 
514
537
  const project = await loadProject({
515
- settingsFilePath: "./project.inlang.json",
538
+ settingsFilePath: "/user/project/project.inlang.json",
516
539
  nodeishFs: fs,
517
540
  _import,
518
541
  })
@@ -639,14 +662,79 @@ describe("functionality", () => {
639
662
  },
640
663
  ])
641
664
  })
665
+
666
+ /*
667
+ * Passing all messages to saveMessages() simplifies plugins by an order of magnitude.
668
+ *
669
+ * The alternative would be to pass only the messages that changed to saveMessages().
670
+ * But, this would require plugins to maintain a separate data structure of messages
671
+ * and create optimizations, leading to (unjustified) complexity for plugin authors.
672
+ *
673
+ * Pros:
674
+ * - plugins don't need to transform the data (time complexity).
675
+ * - plugins don't to maintain a separate data structure (space complexity).
676
+ * - plugin authors don't need to deal with optimizations (ecosystem complexity).
677
+ *
678
+ * Cons:
679
+ * - Might be slow for a large number of messages. The requirement hasn't popped up yet though.
680
+ */
681
+ it("should pass all messages, regardless of which message changed, to saveMessages()", async () => {
682
+ const fs = createNodeishMemoryFs()
683
+
684
+ const settings: ProjectSettings = {
685
+ sourceLanguageTag: "en",
686
+ languageTags: ["en", "de"],
687
+ modules: ["plugin.js"],
688
+ "plugin.project.json": {
689
+ pathPattern: "./resources/{languageTag}.json",
690
+ },
691
+ }
692
+
693
+ await fs.mkdir("/user/project", { recursive: true })
694
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
695
+
696
+ const mockSaveFn = vi.fn()
697
+
698
+ const _mockPlugin: Plugin = {
699
+ id: "plugin.project.json",
700
+ description: "Mock plugin description",
701
+ displayName: "Mock Plugin",
702
+ loadMessages: () => [
703
+ createMessage("first", { en: "first message" }),
704
+ createMessage("second", { en: "second message" }),
705
+ createMessage("third", { en: "third message" }),
706
+ ],
707
+ saveMessages: mockSaveFn,
708
+ }
709
+
710
+ const _import = async () => {
711
+ return {
712
+ default: _mockPlugin,
713
+ } satisfies InlangModule
714
+ }
715
+
716
+ const project = await loadProject({
717
+ settingsFilePath: "/user/project/project.inlang.json",
718
+ nodeishFs: fs,
719
+ _import,
720
+ })
721
+
722
+ project.query.messages.create({ data: createMessage("fourth", { en: "fourth message" }) })
723
+
724
+ await new Promise((resolve) => setTimeout(resolve, 510))
725
+
726
+ expect(mockSaveFn.mock.calls.length).toBe(1)
727
+ expect(mockSaveFn.mock.calls[0][0].messages).toHaveLength(4)
728
+ })
642
729
  })
643
730
 
644
731
  describe("lint", () => {
645
732
  it.todo("should throw if lint reports are not initialized yet", async () => {
646
733
  const fs = await createNodeishMemoryFs()
647
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
734
+ await fs.mkdir("/user/project", { recursive: true })
735
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
648
736
  const project = await loadProject({
649
- settingsFilePath: "./project.inlang.json",
737
+ settingsFilePath: "/user/project/project.inlang.json",
650
738
  nodeishFs: fs,
651
739
  _import,
652
740
  })
@@ -665,9 +753,10 @@ describe("functionality", () => {
665
753
  modules: ["lintRule.js"],
666
754
  }
667
755
  const fs = createNodeishMemoryFs()
668
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
756
+ await fs.mkdir("/user/project", { recursive: true })
757
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings))
669
758
  const project = await loadProject({
670
- settingsFilePath: "./project.inlang.json",
759
+ settingsFilePath: "/user/project/project.inlang.json",
671
760
  nodeishFs: fs,
672
761
  _import: async () => ({
673
762
  default: mockMessageLintRule,
@@ -21,14 +21,18 @@ import { createMessageLintReportsQuery } from "./createMessageLintReportsQuery.j
21
21
  import { ProjectSettings, Message, type NodeishFilesystemSubset } from "./versionedInterfaces.js"
22
22
  import { tryCatch, type Result } from "@inlang/result"
23
23
  import { migrateIfOutdated } from "@inlang/project-settings/migration"
24
+ import { createNodeishFsWithAbsolutePaths } from "./resolve-modules/createNodeishFsWithAbsolutePaths.js"
24
25
 
25
26
  const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
26
27
 
27
28
  /**
28
29
  * Creates an inlang instance.
29
30
  *
30
- * - Use `_import` to pass a custom import function for testing,
31
+ * @param settingsFilePath - Absolute path to the inlang settings file.
32
+ * @param nodeishFs - Filesystem that implements the NodeishFilesystemSubset interface.
33
+ * @param _import - Use `_import` to pass a custom import function for testing,
31
34
  * and supporting legacy resolvedModules such as CJS.
35
+ * @param _capture - Use `_capture` to capture events for analytics.
32
36
  *
33
37
  */
34
38
  export const loadProject = async (args: {
@@ -40,11 +44,18 @@ export const loadProject = async (args: {
40
44
  return await createRoot(async () => {
41
45
  const [initialized, markInitAsComplete, markInitAsFailed] = createAwaitable()
42
46
 
47
+ // -- absolute path env -----------------------------------------------
48
+
49
+ const nodeishFs = createNodeishFsWithAbsolutePaths({
50
+ nodeishFs: args.nodeishFs,
51
+ basePath: args.settingsFilePath.replace(/(.*?)[^/]*\..*$/, "$1"), // get directory of settings file
52
+ })
53
+
43
54
  // -- settings ------------------------------------------------------------
44
55
 
45
56
  const [settings, _setSettings] = createSignal<ProjectSettings>()
46
57
  createEffect(() => {
47
- loadSettings({ settingsFilePath: args.settingsFilePath, nodeishFs: args.nodeishFs })
58
+ loadSettings({ settingsFilePath: args.settingsFilePath, nodeishFs })
48
59
  .then((settings) => {
49
60
  setSettings(settings)
50
61
  // rename settings to get a convenient access to the data in Posthog
@@ -58,7 +69,7 @@ export const loadProject = async (args: {
58
69
  // TODO: create FS watcher and update settings on change
59
70
 
60
71
  const writeSettingsToDisk = skipFirst((settings: ProjectSettings) =>
61
- _writeSettingsToDisk({ nodeishFs: args.nodeishFs, settings })
72
+ _writeSettingsToDisk({ nodeishFs, settings })
62
73
  )
63
74
 
64
75
  const setSettings = (settings: ProjectSettings): Result<void, ProjectSettingsInvalidError> => {
@@ -73,7 +84,9 @@ export const loadProject = async (args: {
73
84
  return { error }
74
85
  }
75
86
 
76
- throw new Error("unhandled")
87
+ throw new Error(
88
+ "Unhandled error in setSettings. This is an internal bug. Please file an issue."
89
+ )
77
90
  }
78
91
  }
79
92
 
@@ -86,7 +99,7 @@ export const loadProject = async (args: {
86
99
  const _settings = settings()
87
100
  if (!_settings) return
88
101
 
89
- resolveModules({ settings: _settings, nodeishFs: args.nodeishFs, _import: args._import })
102
+ resolveModules({ settings: _settings, nodeishFs, _import: args._import })
90
103
  .then((resolvedModules) => {
91
104
  setResolvedModules(resolvedModules)
92
105
  })
@@ -120,9 +133,7 @@ export const loadProject = async (args: {
120
133
  setMessages(messages)
121
134
  markInitAsComplete()
122
135
  })
123
- .catch((err) =>
124
- markInitAsFailed(new PluginLoadMessagesError("Error in load messages", { cause: err }))
125
- )
136
+ .catch((err) => markInitAsFailed(new PluginLoadMessagesError({ cause: err })))
126
137
  })
127
138
 
128
139
  // -- installed items ----------------------------------------------------
@@ -178,7 +189,7 @@ export const loadProject = async (args: {
178
189
  messages: newMessages,
179
190
  })
180
191
  } catch (err) {
181
- throw new PluginSaveMessagesError("Error in saving messages", {
192
+ throw new PluginSaveMessagesError({
182
193
  cause: err,
183
194
  })
184
195
  }
@@ -231,18 +242,17 @@ const loadSettings = async (args: {
231
242
  async () => await args.nodeishFs.readFile(args.settingsFilePath, { encoding: "utf-8" })
232
243
  )
233
244
  if (settingsFileError)
234
- throw new ProjectSettingsFileNotFoundError(
235
- `Could not locate settings file in (${args.settingsFilePath}).`,
236
- {
237
- cause: settingsFileError,
238
- }
239
- )
245
+ throw new ProjectSettingsFileNotFoundError({
246
+ cause: settingsFileError,
247
+ path: args.settingsFilePath,
248
+ })
240
249
 
241
250
  const json = tryCatch(() => JSON.parse(settingsFile!))
242
251
 
243
252
  if (json.error) {
244
- throw new ProjectSettingsFileJSONSyntaxError(`The settings is not a valid JSON file.`, {
253
+ throw new ProjectSettingsFileJSONSyntaxError({
245
254
  cause: json.error,
255
+ path: args.settingsFilePath,
246
256
  })
247
257
  }
248
258
  return parseSettings(json.data)
@@ -253,8 +263,8 @@ const parseSettings = (settings: unknown) => {
253
263
  if (settingsCompiler.Check(withMigration) === false) {
254
264
  const typeErrors = [...settingsCompiler.Errors(settings)]
255
265
  if (typeErrors.length > 0) {
256
- throw new ProjectSettingsInvalidError(`The settings is invalid according to the schema.`, {
257
- cause: typeErrors,
266
+ throw new ProjectSettingsInvalidError({
267
+ errors: typeErrors,
258
268
  })
259
269
  }
260
270
  }
@@ -328,3 +338,5 @@ export function createSubscribable<T>(signal: () => T): Subscribable<T> {
328
338
  },
329
339
  })
330
340
  }
341
+
342
+