@inlang/sdk 0.10.0 → 0.12.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.
@@ -72,9 +72,10 @@ const $import = async (name) => ({
72
72
  describe("config", () => {
73
73
  it("should react to changes in config", async () => {
74
74
  const fs = createNodeishMemoryFs();
75
- await fs.writeFile("./project.inlang.json", JSON.stringify(config));
75
+ await fs.mkdir("/user/project", { recursive: true });
76
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(config));
76
77
  const project = solidAdapter(await loadProject({
77
- settingsFilePath: "./project.inlang.json",
78
+ settingsFilePath: "/user/project/project.inlang.json",
78
79
  nodeishFs: fs,
79
80
  _import: $import,
80
81
  }), { from });
@@ -95,9 +96,10 @@ describe("config", () => {
95
96
  describe("installed", () => {
96
97
  it("react to changes that are unrelated to installed items", async () => {
97
98
  const fs = createNodeishMemoryFs();
98
- await fs.writeFile("./project.inlang.json", JSON.stringify(config));
99
+ await fs.mkdir("/user/project", { recursive: true });
100
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(config));
99
101
  const project = solidAdapter(await loadProject({
100
- settingsFilePath: "./project.inlang.json",
102
+ settingsFilePath: "/user/project/project.inlang.json",
101
103
  nodeishFs: fs,
102
104
  _import: $import,
103
105
  }), { from });
@@ -147,9 +149,10 @@ describe("messages", () => {
147
149
  saveMessages: () => undefined,
148
150
  };
149
151
  const mockImport = async () => ({ default: mockPlugin });
150
- await fs.writeFile("./project.inlang.json", JSON.stringify(mockConfig));
152
+ await fs.mkdir("/user/project", { recursive: true });
153
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(mockConfig));
151
154
  const project = solidAdapter(await loadProject({
152
- settingsFilePath: "./project.inlang.json",
155
+ settingsFilePath: "/user/project/project.inlang.json",
153
156
  nodeishFs: fs,
154
157
  _import: mockImport,
155
158
  }), { from });
@@ -167,9 +170,10 @@ describe("messages", () => {
167
170
  });
168
171
  it("should react to changes in messages", async () => {
169
172
  const fs = createNodeishMemoryFs();
170
- await fs.writeFile("./project.inlang.json", JSON.stringify(config));
173
+ await fs.mkdir("/user/project", { recursive: true });
174
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(config));
171
175
  const project = solidAdapter(await loadProject({
172
- settingsFilePath: "./project.inlang.json",
176
+ settingsFilePath: "/user/project/project.inlang.json",
173
177
  nodeishFs: fs,
174
178
  _import: $import,
175
179
  }), { from });
@@ -0,0 +1,13 @@
1
+ import type { NodeishFilesystemSubset } from "@inlang/plugin";
2
+ export declare const isAbsolutePath: (path: string) => boolean;
3
+ /**
4
+ * Wraps the nodeish filesystem subset with a function that intercepts paths
5
+ * and prepends the base path.
6
+ *
7
+ * The paths are resolved from the `settingsFilePath` argument.
8
+ */
9
+ export declare const createNodeishFsWithAbsolutePaths: (args: {
10
+ settingsFilePath: string;
11
+ nodeishFs: NodeishFilesystemSubset;
12
+ }) => NodeishFilesystemSubset;
13
+ //# sourceMappingURL=createNodeishFsWithAbsolutePaths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createNodeishFsWithAbsolutePaths.d.ts","sourceRoot":"","sources":["../src/createNodeishFsWithAbsolutePaths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAG7D,eAAO,MAAM,cAAc,SAAU,MAAM,YAAwB,CAAA;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,SAAU;IACtD,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,uBAAuB,CAAA;CAClC,KAAG,uBAyBH,CAAA"}
@@ -0,0 +1,29 @@
1
+ import { normalizePath } from "@lix-js/fs";
2
+ export const isAbsolutePath = (path) => /^[/\\]/.test(path);
3
+ /**
4
+ * Wraps the nodeish filesystem subset with a function that intercepts paths
5
+ * and prepends the base path.
6
+ *
7
+ * The paths are resolved from the `settingsFilePath` argument.
8
+ */
9
+ export const createNodeishFsWithAbsolutePaths = (args) => {
10
+ if (!isAbsolutePath(args.settingsFilePath)) {
11
+ throw new Error("The argument `settingsFilePath` must be an absolute path.");
12
+ }
13
+ // get the base path of the settings file by
14
+ // removing the file name from the path
15
+ const bathPath = args.settingsFilePath.split("/").slice(0, -1).join("/");
16
+ const makeAbsolute = (path) => {
17
+ if (isAbsolutePath(path)) {
18
+ return path;
19
+ }
20
+ return normalizePath(bathPath + "/" + path);
21
+ };
22
+ return {
23
+ // @ts-expect-error
24
+ readFile: (path, options) => args.nodeishFs.readFile(makeAbsolute(path), options),
25
+ readdir: (path) => args.nodeishFs.readdir(makeAbsolute(path)),
26
+ mkdir: (path) => args.nodeishFs.mkdir(makeAbsolute(path)),
27
+ writeFile: (path, data) => args.nodeishFs.writeFile(makeAbsolute(path), data),
28
+ };
29
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=createNodeishFsWithAbsolutePaths.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createNodeishFsWithAbsolutePaths.test.d.ts","sourceRoot":"","sources":["../src/createNodeishFsWithAbsolutePaths.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,37 @@
1
+ import { it, expect, vi } from "vitest";
2
+ import { createNodeishFsWithAbsolutePaths } from "./createNodeishFsWithAbsolutePaths.js";
3
+ it("throws an error if settingsFilePath is not an absolute path", () => {
4
+ const relativePath = "relative/path";
5
+ expect(() => createNodeishFsWithAbsolutePaths({ settingsFilePath: relativePath, nodeishFs: {} })).toThrow();
6
+ });
7
+ it("intercepts paths correctly for readFile", async () => {
8
+ const settingsFilePath = `/Users/samuel/Documents/paraglide/example/project.inlang.json`;
9
+ const filePaths = [
10
+ ["file.txt", `/Users/samuel/Documents/paraglide/example/file.txt`],
11
+ ["./file.txt", `/Users/samuel/Documents/paraglide/example/file.txt`],
12
+ ["./folder/file.txt", `/Users/samuel/Documents/paraglide/example/folder/file.txt`],
13
+ ["../file.txt", `/Users/samuel/Documents/paraglide/file.txt`],
14
+ ["../folder/file.txt", `/Users/samuel/Documents/paraglide/folder/file.txt`],
15
+ ["../../file.txt", `/Users/samuel/Documents/file.txt`],
16
+ ["../../../file.txt", `/Users/samuel/file.txt`],
17
+ ];
18
+ const mockNodeishFs = {
19
+ readFile: vi.fn(),
20
+ readdir: vi.fn(),
21
+ mkdir: vi.fn(),
22
+ writeFile: vi.fn(),
23
+ };
24
+ const interceptedFs = createNodeishFsWithAbsolutePaths({
25
+ settingsFilePath,
26
+ nodeishFs: mockNodeishFs,
27
+ });
28
+ for (const [path, expectedPath] of filePaths) {
29
+ for (const fn of Object.keys(mockNodeishFs)) {
30
+ // @ts-expect-error
31
+ await interceptedFs[fn](path);
32
+ // @ts-expect-error
33
+ // expect the first argument to be the expectedPath
34
+ expect(mockNodeishFs[fn].mock.lastCall[0]).toBe(expectedPath);
35
+ }
36
+ }
37
+ });
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  import type { ValueError } from "@sinclair/typebox/errors";
2
+ export declare class LoadProjectInvalidArgument extends Error {
3
+ constructor(message: string, options: {
4
+ argument: string;
5
+ });
6
+ }
2
7
  export declare class ProjectSettingsInvalidError extends Error {
3
8
  constructor(options: {
4
9
  errors: ValueError[];
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAE1D,qBAAa,2BAA4B,SAAQ,KAAK;gBACzC,OAAO,EAAE;QAAE,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;CAQ7C;AAED,qBAAa,kCAAmC,SAAQ,KAAK;gBAChD,OAAO,EAAE;QAAE,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;CAOnE;AAED,qBAAa,gCAAiC,SAAQ,KAAK;gBAC9C,OAAO,EAAE;QAAE,KAAK,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;CAIpE;AAED,qBAAa,uBAAwB,SAAQ,KAAK;gBACrC,OAAO,EAAE;QAAE,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAA;KAAE;CAIrD;AAED,qBAAa,uBAAwB,SAAQ,KAAK;gBACrC,OAAO,EAAE;QAAE,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAA;KAAE;CAIrD"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAE1D,qBAAa,0BAA2B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE;CAI1D;AAED,qBAAa,2BAA4B,SAAQ,KAAK;gBACzC,OAAO,EAAE;QAAE,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;CAQ7C;AAED,qBAAa,kCAAmC,SAAQ,KAAK;gBAChD,OAAO,EAAE;QAAE,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;CAOnE;AAED,qBAAa,gCAAiC,SAAQ,KAAK;gBAC9C,OAAO,EAAE;QAAE,KAAK,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;CAIpE;AAED,qBAAa,uBAAwB,SAAQ,KAAK;gBACrC,OAAO,EAAE;QAAE,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAA;KAAE;CAIrD;AAED,qBAAa,uBAAwB,SAAQ,KAAK;gBACrC,OAAO,EAAE;QAAE,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAA;KAAE;CAIrD"}
package/dist/errors.js CHANGED
@@ -1,3 +1,9 @@
1
+ export class LoadProjectInvalidArgument extends Error {
2
+ constructor(message, options) {
3
+ super(`The argument "${options.argument}" of loadProject() is invalid: ${message}`);
4
+ this.name = "LoadProjectInvalidArgument";
5
+ }
6
+ }
1
7
  export class ProjectSettingsInvalidError extends Error {
2
8
  constructor(options) {
3
9
  super(`The project settings are invalid:\n\n${options.errors
@@ -4,8 +4,11 @@ import { type NodeishFilesystemSubset } from "./versionedInterfaces.js";
4
4
  /**
5
5
  * Creates an inlang instance.
6
6
  *
7
- * - Use `_import` to pass a custom import function for testing,
7
+ * @param settingsFilePath - Absolute path to the inlang settings file.
8
+ * @param nodeishFs - Filesystem that implements the NodeishFilesystemSubset interface.
9
+ * @param _import - Use `_import` to pass a custom import function for testing,
8
10
  * and supporting legacy resolvedModules such as CJS.
11
+ * @param _capture - Use `_capture` to capture events for analytics.
9
12
  *
10
13
  */
11
14
  export declare const loadProject: (args: {
@@ -1 +1 @@
1
- {"version":3,"file":"loadProject.d.ts","sourceRoot":"","sources":["../src/loadProject.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,aAAa,EAGb,YAAY,EACZ,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,cAAc,EAAkB,MAAM,4BAA4B,CAAA;AAahF,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAMjG;;;;;;GAMG;AACH,eAAO,MAAM,WAAW;sBACL,MAAM;eACb,uBAAuB;;qBAElB,MAAM,SAAS,OAAO,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI;MAC5D,QAAQ,aAAa,CAqLxB,CAAA;AAqGD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAQtE"}
1
+ {"version":3,"file":"loadProject.d.ts","sourceRoot":"","sources":["../src/loadProject.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,aAAa,EAGb,YAAY,EACZ,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,cAAc,EAAkB,MAAM,4BAA4B,CAAA;AAchF,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAUjG;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW;sBACL,MAAM;eACb,uBAAuB;;qBAElB,MAAM,SAAS,OAAO,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI;MAC5D,QAAQ,aAAa,CAkMxB,CAAA;AAqGD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAQtE"}
@@ -1,6 +1,6 @@
1
1
  import { resolveModules } from "./resolve-modules/index.js";
2
2
  import { TypeCompiler } from "@sinclair/typebox/compiler";
3
- import { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginLoadMessagesError, PluginSaveMessagesError, } from "./errors.js";
3
+ import { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginLoadMessagesError, PluginSaveMessagesError, LoadProjectInvalidArgument, } from "./errors.js";
4
4
  import { createRoot, createSignal, createEffect } from "./reactivity/solid.js";
5
5
  import { createMessagesQuery } from "./createMessagesQuery.js";
6
6
  import { debounce } from "throttle-debounce";
@@ -8,21 +8,34 @@ import { createMessageLintReportsQuery } from "./createMessageLintReportsQuery.j
8
8
  import { ProjectSettings, Message } from "./versionedInterfaces.js";
9
9
  import { tryCatch } from "@inlang/result";
10
10
  import { migrateIfOutdated } from "@inlang/project-settings/migration";
11
+ import { createNodeishFsWithAbsolutePaths, isAbsolutePath, } from "./createNodeishFsWithAbsolutePaths.js";
11
12
  const settingsCompiler = TypeCompiler.Compile(ProjectSettings);
12
13
  /**
13
14
  * Creates an inlang instance.
14
15
  *
15
- * - Use `_import` to pass a custom import function for testing,
16
+ * @param settingsFilePath - Absolute path to the inlang settings file.
17
+ * @param nodeishFs - Filesystem that implements the NodeishFilesystemSubset interface.
18
+ * @param _import - Use `_import` to pass a custom import function for testing,
16
19
  * and supporting legacy resolvedModules such as CJS.
20
+ * @param _capture - Use `_capture` to capture events for analytics.
17
21
  *
18
22
  */
19
23
  export const loadProject = async (args) => {
24
+ // -- validation --------------------------------------------------------
25
+ //! the only place where throwing is acceptable because the project
26
+ //! won't even be loaded. do not throw anywhere else. otherwise, apps
27
+ //! can't handle errors gracefully.
28
+ if (!isAbsolutePath(args.settingsFilePath)) {
29
+ throw new LoadProjectInvalidArgument(`Expected an absolute path but received "${args.settingsFilePath}".`, { argument: "settingsFilePath" });
30
+ }
31
+ // -- load project ------------------------------------------------------
20
32
  return await createRoot(async () => {
21
33
  const [initialized, markInitAsComplete, markInitAsFailed] = createAwaitable();
34
+ const nodeishFs = createNodeishFsWithAbsolutePaths(args);
22
35
  // -- settings ------------------------------------------------------------
23
36
  const [settings, _setSettings] = createSignal();
24
37
  createEffect(() => {
25
- loadSettings({ settingsFilePath: args.settingsFilePath, nodeishFs: args.nodeishFs })
38
+ loadSettings({ settingsFilePath: args.settingsFilePath, nodeishFs })
26
39
  .then((settings) => {
27
40
  setSettings(settings);
28
41
  // rename settings to get a convenient access to the data in Posthog
@@ -34,7 +47,7 @@ export const loadProject = async (args) => {
34
47
  });
35
48
  });
36
49
  // TODO: create FS watcher and update settings on change
37
- const writeSettingsToDisk = skipFirst((settings) => _writeSettingsToDisk({ nodeishFs: args.nodeishFs, settings }));
50
+ const writeSettingsToDisk = skipFirst((settings) => _writeSettingsToDisk({ nodeishFs, settings }));
38
51
  const setSettings = (settings) => {
39
52
  try {
40
53
  const validatedSettings = parseSettings(settings);
@@ -55,7 +68,7 @@ export const loadProject = async (args) => {
55
68
  const _settings = settings();
56
69
  if (!_settings)
57
70
  return;
58
- resolveModules({ settings: _settings, nodeishFs: args.nodeishFs, _import: args._import })
71
+ resolveModules({ settings: _settings, nodeishFs, _import: args._import })
59
72
  .then((resolvedModules) => {
60
73
  setResolvedModules(resolvedModules);
61
74
  })
@@ -1,8 +1,10 @@
1
1
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
2
  import { describe, it, expect, vi } from "vitest";
3
3
  import { loadProject } from "./loadProject.js";
4
- import { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, } from "./errors.js";
4
+ import { LoadProjectInvalidArgument, ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, } from "./errors.js";
5
5
  import { createNodeishMemoryFs } from "@lix-js/fs";
6
+ import { createMessage } from "./test-utilities/createMessage.js";
7
+ import { tryCatch } from "@inlang/result";
6
8
  // ------------------------------------------------------------------------------------------------
7
9
  const getValue = (subscribable) => {
8
10
  let value;
@@ -79,11 +81,22 @@ const _import = async (name) => ({
79
81
  });
80
82
  // ------------------------------------------------------------------------------------------------
81
83
  describe("initialization", () => {
84
+ it("should throw if settingsFilePath is not an absolute path", async () => {
85
+ const fs = createNodeishMemoryFs();
86
+ const result = await tryCatch(() => loadProject({
87
+ settingsFilePath: "relative/path",
88
+ nodeishFs: fs,
89
+ _import,
90
+ }));
91
+ expect(result.error).toBeInstanceOf(LoadProjectInvalidArgument);
92
+ expect(result.data).toBeUndefined();
93
+ });
82
94
  describe("settings", () => {
83
95
  it("should return an error if settings file is not found", async () => {
84
96
  const fs = createNodeishMemoryFs();
97
+ fs.mkdir("/user/project", { recursive: true });
85
98
  const project = await loadProject({
86
- settingsFilePath: "./test.json",
99
+ settingsFilePath: "/user/project/test.json",
87
100
  nodeishFs: fs,
88
101
  _import,
89
102
  });
@@ -91,9 +104,10 @@ describe("initialization", () => {
91
104
  });
92
105
  it("should return an error if settings file is not a valid JSON", async () => {
93
106
  const fs = await createNodeishMemoryFs();
94
- await fs.writeFile("./project.inlang.json", "invalid json");
107
+ await fs.mkdir("/user/project", { recursive: true });
108
+ await fs.writeFile("/user/project/project.inlang.json", "invalid json");
95
109
  const project = await loadProject({
96
- settingsFilePath: "./project.inlang.json",
110
+ settingsFilePath: "/user/project/project.inlang.json",
97
111
  nodeishFs: fs,
98
112
  _import,
99
113
  });
@@ -101,9 +115,10 @@ describe("initialization", () => {
101
115
  });
102
116
  it("should return an error if settings file is does not match schema", async () => {
103
117
  const fs = await createNodeishMemoryFs();
104
- await fs.writeFile("./project.inlang.json", JSON.stringify({}));
118
+ await fs.mkdir("/user/project", { recursive: true });
119
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify({}));
105
120
  const project = await loadProject({
106
- settingsFilePath: "./project.inlang.json",
121
+ settingsFilePath: "/user/project/project.inlang.json",
107
122
  nodeishFs: fs,
108
123
  _import,
109
124
  });
@@ -111,9 +126,10 @@ describe("initialization", () => {
111
126
  });
112
127
  it("should return the parsed settings", async () => {
113
128
  const fs = await createNodeishMemoryFs();
114
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
129
+ await fs.mkdir("/user/project", { recursive: true });
130
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
115
131
  const project = await loadProject({
116
- settingsFilePath: "./project.inlang.json",
132
+ settingsFilePath: "/user/project/project.inlang.json",
117
133
  nodeishFs: fs,
118
134
  _import,
119
135
  });
@@ -122,18 +138,23 @@ describe("initialization", () => {
122
138
  it("should not re-write the settings to disk when initializing", async () => {
123
139
  const fs = await createNodeishMemoryFs();
124
140
  const settingsWithDeifferentFormatting = JSON.stringify(settings, undefined, 4);
125
- await fs.writeFile("./project.inlang.json", settingsWithDeifferentFormatting);
141
+ await fs.mkdir("/user/project", { recursive: true });
142
+ await fs.writeFile("/user/project/project.inlang.json", settingsWithDeifferentFormatting);
126
143
  const project = await loadProject({
127
- settingsFilePath: "./project.inlang.json",
144
+ settingsFilePath: "/user/project/project.inlang.json",
128
145
  nodeishFs: fs,
129
146
  _import,
130
147
  });
131
- const settingsOnDisk = await fs.readFile("./project.inlang.json", { encoding: "utf-8" });
148
+ const settingsOnDisk = await fs.readFile("/user/project/project.inlang.json", {
149
+ encoding: "utf-8",
150
+ });
132
151
  expect(settingsOnDisk).toBe(settingsWithDeifferentFormatting);
133
152
  project.setSettings(project.settings());
134
153
  // TODO: how can we await `setsettings` correctly
135
154
  await new Promise((resolve) => setTimeout(resolve, 0));
136
- const newsettingsOnDisk = await fs.readFile("./project.inlang.json", { encoding: "utf-8" });
155
+ const newsettingsOnDisk = await fs.readFile("/user/project/project.inlang.json", {
156
+ encoding: "utf-8",
157
+ });
137
158
  expect(newsettingsOnDisk).not.toBe(settingsWithDeifferentFormatting);
138
159
  });
139
160
  });
@@ -143,9 +164,10 @@ describe("initialization", () => {
143
164
  default: {},
144
165
  });
145
166
  const fs = createNodeishMemoryFs();
146
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
167
+ await fs.mkdir("/user/project", { recursive: true });
168
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
147
169
  const project = await loadProject({
148
- settingsFilePath: "./project.inlang.json",
170
+ settingsFilePath: "/user/project/project.inlang.json",
149
171
  nodeishFs: fs,
150
172
  _import: $badImport,
151
173
  });
@@ -170,9 +192,10 @@ describe("functionality", () => {
170
192
  describe("settings", () => {
171
193
  it("should return the settings", async () => {
172
194
  const fs = await createNodeishMemoryFs();
173
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
195
+ await fs.mkdir("/user/project", { recursive: true });
196
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
174
197
  const project = await loadProject({
175
- settingsFilePath: "./project.inlang.json",
198
+ settingsFilePath: "/user/project/project.inlang.json",
176
199
  nodeishFs: fs,
177
200
  _import,
178
201
  });
@@ -180,9 +203,10 @@ describe("functionality", () => {
180
203
  });
181
204
  it("should set a new settings", async () => {
182
205
  const fs = await createNodeishMemoryFs();
183
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
206
+ await fs.mkdir("/user/project", { recursive: true });
207
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
184
208
  const project = await loadProject({
185
- settingsFilePath: "./project.inlang.json",
209
+ settingsFilePath: "/user/project/project.inlang.json",
186
210
  nodeishFs: fs,
187
211
  _import,
188
212
  });
@@ -201,9 +225,10 @@ describe("functionality", () => {
201
225
  describe("setSettings", () => {
202
226
  it("should fail if settings is not valid", async () => {
203
227
  const fs = await createNodeishMemoryFs();
204
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
228
+ await fs.mkdir("/user/project", { recursive: true });
229
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
205
230
  const project = await loadProject({
206
- settingsFilePath: "./project.inlang.json",
231
+ settingsFilePath: "/user/project/project.inlang.json",
207
232
  nodeishFs: fs,
208
233
  _import,
209
234
  });
@@ -213,20 +238,21 @@ describe("functionality", () => {
213
238
  });
214
239
  it("should write settings to disk", async () => {
215
240
  const fs = await createNodeishMemoryFs();
216
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
241
+ await fs.mkdir("/user/project", { recursive: true });
242
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
217
243
  const project = await loadProject({
218
- settingsFilePath: "./project.inlang.json",
244
+ settingsFilePath: "/user/project/project.inlang.json",
219
245
  nodeishFs: fs,
220
246
  _import,
221
247
  });
222
- const before = await fs.readFile("./project.inlang.json", { encoding: "utf-8" });
248
+ const before = await fs.readFile("/user/project/project.inlang.json", { encoding: "utf-8" });
223
249
  expect(before).toBeDefined();
224
250
  const result = project.setSettings({ ...settings, languageTags: [] });
225
251
  expect(result.data).toBeUndefined();
226
252
  expect(result.error).toBeUndefined();
227
253
  // TODO: how to wait for fs.writeFile to finish?
228
254
  await new Promise((resolve) => setTimeout(resolve, 0));
229
- const after = await fs.readFile("./project.inlang.json", { encoding: "utf-8" });
255
+ const after = await fs.readFile("/user/project/project.inlang.json", { encoding: "utf-8" });
230
256
  expect(after).toBeDefined();
231
257
  expect(after).not.toBe(before);
232
258
  });
@@ -239,9 +265,10 @@ describe("functionality", () => {
239
265
  languageTags: ["en"],
240
266
  modules: ["plugin.js", "lintRule.js"],
241
267
  };
242
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
268
+ await fs.mkdir("/user/project", { recursive: true });
269
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
243
270
  const project = await loadProject({
244
- settingsFilePath: "./project.inlang.json",
271
+ settingsFilePath: "/user/project/project.inlang.json",
245
272
  nodeishFs: fs,
246
273
  _import,
247
274
  });
@@ -266,9 +293,10 @@ describe("functionality", () => {
266
293
  languageTags: ["en"],
267
294
  modules: ["plugin.js", "lintRule.js"],
268
295
  };
269
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
296
+ await fs.mkdir("/user/project", { recursive: true });
297
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
270
298
  const project = await loadProject({
271
- settingsFilePath: "./project.inlang.json",
299
+ settingsFilePath: "/user/project/project.inlang.json",
272
300
  nodeishFs: fs,
273
301
  _import,
274
302
  });
@@ -296,7 +324,8 @@ describe("functionality", () => {
296
324
  saveMessages: () => undefined,
297
325
  };
298
326
  const fs = await createNodeishMemoryFs();
299
- await fs.writeFile("./project.inlang.json", JSON.stringify({
327
+ await fs.mkdir("/user/project", { recursive: true });
328
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify({
300
329
  sourceLanguageTag: "en",
301
330
  languageTags: ["en"],
302
331
  modules: ["plugin.js", "lintRule.js"],
@@ -307,7 +336,7 @@ describe("functionality", () => {
307
336
  };
308
337
  };
309
338
  const project = await loadProject({
310
- settingsFilePath: "./project.inlang.json",
339
+ settingsFilePath: "/user/project/project.inlang.json",
311
340
  nodeishFs: fs,
312
341
  _import,
313
342
  });
@@ -337,7 +366,8 @@ describe("functionality", () => {
337
366
  saveMessages: () => undefined,
338
367
  };
339
368
  const fs = await createNodeishMemoryFs();
340
- await fs.writeFile("./project.settings.json", JSON.stringify({
369
+ await fs.mkdir("/user/project", { recursive: true });
370
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify({
341
371
  sourceLanguageTag: "en",
342
372
  languageTags: ["en"],
343
373
  modules: ["plugin.js", "lintRule.js"],
@@ -348,7 +378,7 @@ describe("functionality", () => {
348
378
  };
349
379
  };
350
380
  const project = await loadProject({
351
- settingsFilePath: "./project.settings.json",
381
+ settingsFilePath: "/user/project/project.inlang.json",
352
382
  nodeishFs: fs,
353
383
  _import,
354
384
  });
@@ -359,9 +389,10 @@ describe("functionality", () => {
359
389
  describe("errors", () => {
360
390
  it("should return the errors", async () => {
361
391
  const fs = await createNodeishMemoryFs();
362
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
392
+ await fs.mkdir("/user/project", { recursive: true });
393
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
363
394
  const project = await loadProject({
364
- settingsFilePath: "./project.inlang.json",
395
+ settingsFilePath: "/user/project/project.inlang.json",
365
396
  nodeishFs: fs,
366
397
  _import,
367
398
  });
@@ -373,9 +404,10 @@ describe("functionality", () => {
373
404
  describe("customApi", () => {
374
405
  it("should return the app specific api", async () => {
375
406
  const fs = await createNodeishMemoryFs();
376
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
407
+ await fs.mkdir("/user/project", { recursive: true });
408
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
377
409
  const project = await loadProject({
378
- settingsFilePath: "./project.inlang.json",
410
+ settingsFilePath: "/user/project/project.inlang.json",
379
411
  nodeishFs: fs,
380
412
  _import,
381
413
  });
@@ -387,9 +419,10 @@ describe("functionality", () => {
387
419
  describe("messages", () => {
388
420
  it("should return the messages", async () => {
389
421
  const fs = await createNodeishMemoryFs();
390
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
422
+ await fs.mkdir("/user/project", { recursive: true });
423
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
391
424
  const project = await loadProject({
392
- settingsFilePath: "./project.inlang.json",
425
+ settingsFilePath: "/user/project/project.inlang.json",
393
426
  nodeishFs: fs,
394
427
  _import,
395
428
  });
@@ -407,7 +440,8 @@ describe("functionality", () => {
407
440
  pathPattern: "./resources/{languageTag}.json",
408
441
  },
409
442
  };
410
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
443
+ await fs.mkdir("/user/project", { recursive: true });
444
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
411
445
  await fs.mkdir("./resources");
412
446
  const mockSaveFn = vi.fn();
413
447
  const _mockPlugin = {
@@ -423,7 +457,7 @@ describe("functionality", () => {
423
457
  };
424
458
  };
425
459
  const project = await loadProject({
426
- settingsFilePath: "./project.inlang.json",
460
+ settingsFilePath: "/user/project/project.inlang.json",
427
461
  nodeishFs: fs,
428
462
  _import,
429
463
  });
@@ -543,13 +577,68 @@ describe("functionality", () => {
543
577
  },
544
578
  ]);
545
579
  });
580
+ /*
581
+ * Passing all messages to saveMessages() simplifies plugins by an order of magnitude.
582
+ *
583
+ * The alternative would be to pass only the messages that changed to saveMessages().
584
+ * But, this would require plugins to maintain a separate data structure of messages
585
+ * and create optimizations, leading to (unjustified) complexity for plugin authors.
586
+ *
587
+ * Pros:
588
+ * - plugins don't need to transform the data (time complexity).
589
+ * - plugins don't to maintain a separate data structure (space complexity).
590
+ * - plugin authors don't need to deal with optimizations (ecosystem complexity).
591
+ *
592
+ * Cons:
593
+ * - Might be slow for a large number of messages. The requirement hasn't popped up yet though.
594
+ */
595
+ it("should pass all messages, regardless of which message changed, to saveMessages()", async () => {
596
+ const fs = createNodeishMemoryFs();
597
+ const settings = {
598
+ sourceLanguageTag: "en",
599
+ languageTags: ["en", "de"],
600
+ modules: ["plugin.js"],
601
+ "plugin.project.json": {
602
+ pathPattern: "./resources/{languageTag}.json",
603
+ },
604
+ };
605
+ await fs.mkdir("/user/project", { recursive: true });
606
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
607
+ const mockSaveFn = vi.fn();
608
+ const _mockPlugin = {
609
+ id: "plugin.project.json",
610
+ description: "Mock plugin description",
611
+ displayName: "Mock Plugin",
612
+ loadMessages: () => [
613
+ createMessage("first", { en: "first message" }),
614
+ createMessage("second", { en: "second message" }),
615
+ createMessage("third", { en: "third message" }),
616
+ ],
617
+ saveMessages: mockSaveFn,
618
+ };
619
+ const _import = async () => {
620
+ return {
621
+ default: _mockPlugin,
622
+ };
623
+ };
624
+ const project = await loadProject({
625
+ settingsFilePath: "/user/project/project.inlang.json",
626
+ nodeishFs: fs,
627
+ _import,
628
+ });
629
+ project.query.messages.create({ data: createMessage("fourth", { en: "fourth message" }) });
630
+ await new Promise((resolve) => setTimeout(resolve, 510));
631
+ expect(mockSaveFn.mock.calls.length).toBe(1);
632
+ expect(mockSaveFn.mock.calls[0][0].messages).toHaveLength(4);
633
+ });
546
634
  });
547
635
  describe("lint", () => {
548
636
  it.todo("should throw if lint reports are not initialized yet", async () => {
549
637
  const fs = await createNodeishMemoryFs();
550
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
638
+ await fs.mkdir("/user/project", { recursive: true });
639
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
551
640
  const project = await loadProject({
552
- settingsFilePath: "./project.inlang.json",
641
+ settingsFilePath: "/user/project/project.inlang.json",
553
642
  nodeishFs: fs,
554
643
  _import,
555
644
  });
@@ -569,9 +658,10 @@ describe("functionality", () => {
569
658
  modules: ["lintRule.js"],
570
659
  };
571
660
  const fs = createNodeishMemoryFs();
572
- await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
661
+ await fs.mkdir("/user/project", { recursive: true });
662
+ await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(settings));
573
663
  const project = await loadProject({
574
- settingsFilePath: "./project.inlang.json",
664
+ settingsFilePath: "/user/project/project.inlang.json",
575
665
  nodeishFs: fs,
576
666
  _import: async () => ({
577
667
  default: mockMessageLintRule,