@inlang/sdk 0.16.0 → 0.18.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 (33) hide show
  1. package/dist/createNodeishFsWithAbsolutePaths.d.ts.map +1 -1
  2. package/dist/createNodeishFsWithAbsolutePaths.js +1 -0
  3. package/dist/createNodeishFsWithAbsolutePaths.test.js +1 -0
  4. package/dist/createNodeishFsWithWatcher.d.ts +12 -0
  5. package/dist/createNodeishFsWithWatcher.d.ts.map +1 -0
  6. package/dist/createNodeishFsWithWatcher.js +50 -0
  7. package/dist/createNodeishFsWithWatcher.test.d.ts +2 -0
  8. package/dist/createNodeishFsWithWatcher.test.d.ts.map +1 -0
  9. package/dist/createNodeishFsWithWatcher.test.js +32 -0
  10. package/dist/loadProject.d.ts.map +1 -1
  11. package/dist/loadProject.js +31 -16
  12. package/dist/loadProject.test.js +69 -1
  13. package/dist/resolve-modules/errors.d.ts.map +1 -1
  14. package/dist/resolve-modules/errors.js +3 -3
  15. package/dist/resolve-modules/plugins/resolvePlugins.js +1 -1
  16. package/dist/resolve-modules/plugins/resolvePlugins.test.js +3 -21
  17. package/dist/resolve-modules/plugins/types.d.ts +2 -1
  18. package/dist/resolve-modules/plugins/types.d.ts.map +1 -1
  19. package/dist/resolve-modules/validateModuleSettings.test.js +17 -8
  20. package/dist/resolve-modules/validatedModuleSettings.d.ts.map +1 -1
  21. package/package.json +2 -2
  22. package/src/createNodeishFsWithAbsolutePaths.test.ts +1 -0
  23. package/src/createNodeishFsWithAbsolutePaths.ts +4 -0
  24. package/src/createNodeishFsWithWatcher.test.ts +40 -0
  25. package/src/createNodeishFsWithWatcher.ts +56 -0
  26. package/src/loadProject.test.ts +92 -2
  27. package/src/loadProject.ts +36 -20
  28. package/src/resolve-modules/errors.ts +11 -3
  29. package/src/resolve-modules/plugins/resolvePlugins.test.ts +2 -24
  30. package/src/resolve-modules/plugins/resolvePlugins.ts +1 -1
  31. package/src/resolve-modules/plugins/types.ts +5 -2
  32. package/src/resolve-modules/validateModuleSettings.test.ts +17 -8
  33. package/src/resolve-modules/validatedModuleSettings.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"createNodeishFsWithAbsolutePaths.d.ts","sourceRoot":"","sources":["../src/createNodeishFsWithAbsolutePaths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAI7D;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,SAAU;IACtD,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,uBAAuB,CAAA;CAClC,KAAG,uBAyBH,CAAA"}
1
+ {"version":3,"file":"createNodeishFsWithAbsolutePaths.d.ts","sourceRoot":"","sources":["../src/createNodeishFsWithAbsolutePaths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAI7D;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,SAAU;IACtD,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,uBAAuB,CAAA;CAClC,KAAG,uBA6BH,CAAA"}
@@ -25,5 +25,6 @@ export const createNodeishFsWithAbsolutePaths = (args) => {
25
25
  readdir: (path) => args.nodeishFs.readdir(makeAbsolute(path)),
26
26
  mkdir: (path) => args.nodeishFs.mkdir(makeAbsolute(path)),
27
27
  writeFile: (path, data) => args.nodeishFs.writeFile(makeAbsolute(path), data),
28
+ watch: (path, options) => args.nodeishFs.watch(makeAbsolute(path), options),
28
29
  };
29
30
  };
@@ -20,6 +20,7 @@ it("intercepts paths correctly for readFile", async () => {
20
20
  readdir: vi.fn(),
21
21
  mkdir: vi.fn(),
22
22
  writeFile: vi.fn(),
23
+ watch: vi.fn(),
23
24
  };
24
25
  const interceptedFs = createNodeishFsWithAbsolutePaths({
25
26
  settingsFilePath,
@@ -0,0 +1,12 @@
1
+ import type { NodeishFilesystemSubset } from "@inlang/plugin";
2
+ /**
3
+ * Wraps the nodeish filesystem subset with a function that intercepts paths
4
+ * and prepends the base path.
5
+ *
6
+ * The paths are resolved from the `settingsFilePath` argument.
7
+ */
8
+ export declare const createNodeishFsWithWatcher: (args: {
9
+ nodeishFs: NodeishFilesystemSubset;
10
+ updateMessages: () => void;
11
+ }) => NodeishFilesystemSubset;
12
+ //# sourceMappingURL=createNodeishFsWithWatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createNodeishFsWithWatcher.d.ts","sourceRoot":"","sources":["../src/createNodeishFsWithWatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,SAAU;IAChD,SAAS,EAAE,uBAAuB,CAAA;IAClC,cAAc,EAAE,MAAM,IAAI,CAAA;CAC1B,KAAG,uBA4CH,CAAA"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Wraps the nodeish filesystem subset with a function that intercepts paths
3
+ * and prepends the base path.
4
+ *
5
+ * The paths are resolved from the `settingsFilePath` argument.
6
+ */
7
+ export const createNodeishFsWithWatcher = (args) => {
8
+ const pathList = [];
9
+ const makeWatcher = (path) => {
10
+ const abortController = new AbortController();
11
+ (async () => {
12
+ try {
13
+ const watcher = args.nodeishFs.watch(path, {
14
+ signal: abortController.signal,
15
+ persistent: false,
16
+ });
17
+ //eslint-disable-next-line @typescript-eslint/no-unused-vars
18
+ for await (const event of watcher) {
19
+ args.updateMessages();
20
+ }
21
+ }
22
+ catch (err) {
23
+ if (err.name === "AbortError")
24
+ return;
25
+ // https://github.com/inlang/monorepo/issues/1647
26
+ // the file does not exist (yet)
27
+ // this is not testable beacause the fs.watch api differs
28
+ // from node and lix. lenghty
29
+ else if (err.code === "ENOENT")
30
+ return;
31
+ throw err;
32
+ }
33
+ })();
34
+ };
35
+ const readFileAndExtractPath = (path, options) => {
36
+ if (!pathList.includes(path)) {
37
+ makeWatcher(path);
38
+ pathList.push(path);
39
+ }
40
+ return args.nodeishFs.readFile(path, options);
41
+ };
42
+ return {
43
+ // @ts-expect-error
44
+ readFile: (path, options) => readFileAndExtractPath(path, options),
45
+ readdir: args.nodeishFs.readdir,
46
+ mkdir: args.nodeishFs.mkdir,
47
+ writeFile: args.nodeishFs.writeFile,
48
+ watch: args.nodeishFs.watch,
49
+ };
50
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=createNodeishFsWithWatcher.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createNodeishFsWithWatcher.test.d.ts","sourceRoot":"","sources":["../src/createNodeishFsWithWatcher.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,32 @@
1
+ import { createNodeishMemoryFs } from "@lix-js/fs";
2
+ import { describe, it, expect } from "vitest";
3
+ import { createNodeishFsWithWatcher } from "./createNodeishFsWithWatcher.js";
4
+ describe("watcher", () => {
5
+ it("should trigger the update function when file changes", async () => {
6
+ let counter = 0;
7
+ const fs = createNodeishFsWithWatcher({
8
+ nodeishFs: createNodeishMemoryFs(),
9
+ updateMessages: () => {
10
+ counter++;
11
+ },
12
+ });
13
+ // establish watcher
14
+ await fs.writeFile("file.txt", "a");
15
+ await fs.readFile("file.txt", { encoding: "utf-8" });
16
+ expect(counter).toBe(0);
17
+ // initial file change
18
+ await fs.writeFile("file.txt", "b");
19
+ await new Promise((resolve) => setTimeout(resolve, 0));
20
+ expect(counter).toBe(1);
21
+ // change file
22
+ await fs.writeFile("file.txt", "a");
23
+ await new Promise((resolve) => setTimeout(resolve, 0));
24
+ //check if update function was called
25
+ expect(counter).toBe(2);
26
+ // change file
27
+ await fs.readFile("file.txt");
28
+ await new Promise((resolve) => setTimeout(resolve, 0));
29
+ //check if update function was called
30
+ expect(counter).toBe(2);
31
+ });
32
+ });
@@ -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;AAchF,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AASjG;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW;sBACL,MAAM;eACb,uBAAuB;;qBAElB,MAAM,SAAS,OAAO,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI;MAC5D,QAAQ,aAAa,CAuMxB,CAAA;AAuHD,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,CAsNxB,CAAA;AAuHD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAQtE"}
@@ -11,6 +11,7 @@ import { migrateIfOutdated } from "@inlang/project-settings/migration";
11
11
  import { createNodeishFsWithAbsolutePaths } from "./createNodeishFsWithAbsolutePaths.js";
12
12
  import { normalizePath } from "@lix-js/fs";
13
13
  import { isAbsolutePath } from "./isAbsolutePath.js";
14
+ import { createNodeishFsWithWatcher } from "./createNodeishFsWithWatcher.js";
14
15
  const settingsCompiler = TypeCompiler.Compile(ProjectSettings);
15
16
  /**
16
17
  * Creates an inlang instance.
@@ -95,14 +96,24 @@ export const loadProject = async (args) => {
95
96
  markInitAsFailed(undefined);
96
97
  return;
97
98
  }
98
- makeTrulyAsync(_resolvedModules.resolvedPluginApi.loadMessages({
99
- settings: settingsValue,
100
- }))
101
- .then((messages) => {
102
- setMessages(messages);
103
- markInitAsComplete();
104
- })
105
- .catch((err) => markInitAsFailed(new PluginLoadMessagesError({ cause: err })));
99
+ const loadAndSetMessages = async (fs) => {
100
+ makeTrulyAsync(_resolvedModules.resolvedPluginApi.loadMessages({
101
+ settings: settingsValue,
102
+ nodeishFs: fs,
103
+ }))
104
+ .then((messages) => {
105
+ setMessages(messages);
106
+ markInitAsComplete();
107
+ })
108
+ .catch((err) => markInitAsFailed(new PluginLoadMessagesError({ cause: err })));
109
+ };
110
+ const fsWithWatcher = createNodeishFsWithWatcher({
111
+ nodeishFs: nodeishFs,
112
+ updateMessages: () => {
113
+ loadAndSetMessages(nodeishFs);
114
+ },
115
+ });
116
+ loadAndSetMessages(fsWithWatcher);
106
117
  });
107
118
  // -- installed items ----------------------------------------------------
108
119
  const installedMessageLintRules = () => {
@@ -135,20 +146,24 @@ export const loadProject = async (args) => {
135
146
  const lintReportsQuery = createMessageLintReportsQuery(messages, settings, installedMessageLintRules, resolvedModules);
136
147
  const debouncedSave = skipFirst(debounce(500, async (newMessages) => {
137
148
  try {
138
- await resolvedModules()?.resolvedPluginApi.saveMessages({
139
- settings: settingsValue,
140
- messages: newMessages,
141
- });
149
+ if (JSON.stringify(newMessages) !== JSON.stringify(messages())) {
150
+ await resolvedModules()?.resolvedPluginApi.saveMessages({
151
+ settings: settingsValue,
152
+ messages: newMessages,
153
+ });
154
+ }
142
155
  }
143
156
  catch (err) {
144
157
  throw new PluginSaveMessagesError({
145
158
  cause: err,
146
159
  });
147
160
  }
148
- if (newMessages.length !== 0 &&
149
- JSON.stringify(newMessages) !== JSON.stringify(messages())) {
150
- setMessages(newMessages);
151
- }
161
+ // if (
162
+ // newMessages.length !== 0 &&
163
+ // JSON.stringify(newMessages) !== JSON.stringify(messages())
164
+ // ) {
165
+ // setMessages(newMessages)
166
+ // }
152
167
  }, { atBegin: false }));
153
168
  createEffect(() => {
154
169
  debouncedSave(messagesQuery.getAll());
@@ -2,7 +2,7 @@
2
2
  import { describe, it, expect, vi } from "vitest";
3
3
  import { loadProject } from "./loadProject.js";
4
4
  import { LoadProjectInvalidArgument, ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, } from "./errors.js";
5
- import { createNodeishMemoryFs } from "@lix-js/fs";
5
+ import { createNodeishMemoryFs, normalizePath } from "@lix-js/fs";
6
6
  import { createMessage } from "./test-utilities/createMessage.js";
7
7
  import { tryCatch } from "@inlang/result";
8
8
  // ------------------------------------------------------------------------------------------------
@@ -699,4 +699,72 @@ describe("functionality", () => {
699
699
  project.query.messageLintReports.getAll.subscribe((r) => expect(r).toEqual([]));
700
700
  });
701
701
  });
702
+ describe("watcher", () => {
703
+ it("changing files in resources should trigger callback of message query", async () => {
704
+ const fs = createNodeishMemoryFs();
705
+ const messages = {
706
+ $schema: "https://inlang.com/schema/inlang-message-format",
707
+ data: [
708
+ {
709
+ id: "test",
710
+ selectors: [],
711
+ variants: [
712
+ {
713
+ match: [],
714
+ languageTag: "en",
715
+ pattern: [
716
+ {
717
+ type: "Text",
718
+ value: "test",
719
+ },
720
+ ],
721
+ },
722
+ ],
723
+ },
724
+ ],
725
+ };
726
+ await fs.writeFile("./messages.json", JSON.stringify(messages));
727
+ const getMessages = async (customFs) => {
728
+ const file = await customFs.readFile("./messages.json", { encoding: "utf-8" });
729
+ return JSON.parse(file.toString()).data;
730
+ };
731
+ const mockMessageFormatPlugin = {
732
+ id: "plugin.inlang.messageFormat",
733
+ description: { en: "Mock plugin description" },
734
+ displayName: { en: "Mock Plugin" },
735
+ loadMessages: async (args) => await getMessages(args.nodeishFs),
736
+ saveMessages: () => undefined,
737
+ };
738
+ const settings = {
739
+ sourceLanguageTag: "en",
740
+ languageTags: ["en"],
741
+ modules: ["plugin.js"],
742
+ "plugin.inlang.messageFormat": {
743
+ filePath: "./messages.json",
744
+ },
745
+ };
746
+ await fs.writeFile("./project.inlang.json", JSON.stringify(settings));
747
+ // establish watcher
748
+ const project = await loadProject({
749
+ settingsFilePath: normalizePath("/project.inlang.json"),
750
+ nodeishFs: fs,
751
+ _import: async () => ({
752
+ default: mockMessageFormatPlugin,
753
+ }),
754
+ });
755
+ let counter = 0;
756
+ project.query.messages.getAll.subscribe(() => {
757
+ counter = counter + 1;
758
+ });
759
+ expect(counter).toBe(1);
760
+ // change file
761
+ await fs.writeFile("./messages.json", JSON.stringify(messages));
762
+ await new Promise((resolve) => setTimeout(resolve, 0));
763
+ expect(counter).toBe(2);
764
+ // change file
765
+ await fs.writeFile("./messages.json", JSON.stringify(messages));
766
+ await new Promise((resolve) => setTimeout(resolve, 0));
767
+ expect(counter).toBe(3);
768
+ });
769
+ });
702
770
  });
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAC1D,cAAc,qBAAqB,CAAA;AACnC,cAAc,gCAAgC,CAAA;AAE9C,qBAAa,WAAY,SAAQ,KAAK;IACrC,SAAgB,MAAM,EAAE,MAAM,CAAA;gBAElB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAMvE;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,WAAW;gBAC3C,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAOtD;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;gBACrC,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE;CAIrD;AAED,qBAAa,0BAA2B,SAAQ,WAAW;gBAC9C,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;CAS7D;AAED,qBAAa,6BAA8B,SAAQ,WAAW;gBACjD,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;CAS7D"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAC1D,cAAc,qBAAqB,CAAA;AACnC,cAAc,gCAAgC,CAAA;AAE9C,qBAAa,WAAY,SAAQ,KAAK;IACrC,SAAgB,MAAM,EAAE,MAAM,CAAA;gBAElB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAMvE;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,WAAW;gBAC3C,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAOtD;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;gBACrC,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE;CAIrD;AAED,qBAAa,0BAA2B,SAAQ,WAAW;gBAC9C,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;CAY7D;AAED,qBAAa,6BAA8B,SAAQ,WAAW;gBACjD,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;CAc7D"}
@@ -30,15 +30,15 @@ export class ModuleImportError extends ModuleError {
30
30
  export class ModuleExportIsInvalidError extends ModuleError {
31
31
  constructor(options) {
32
32
  super(`The export(s) of "${options.module}" are invalid:\n\n${options.errors
33
- .map((error) => `"${error.path}" "${error.value}": "${error.message}"`)
33
+ .map((error) => `"${error.path}" "${JSON.stringify(error.value, undefined, 2)}": "${error.message}"`)
34
34
  .join("\n")}`, options);
35
35
  this.name = "ModuleExportIsInvalidError";
36
36
  }
37
37
  }
38
38
  export class ModuleSettingsAreInvalidError extends ModuleError {
39
39
  constructor(options) {
40
- super(`The settings are invalid of "${module}" are invalid:\n\n${options.errors
41
- .map((error) => `Path "${error.path}" with value "${error.value}": "${error.message}"`)
40
+ super(`The settings are invalid of "${options.module}" are invalid:\n\n${options.errors
41
+ .map((error) => `Path "${error.path}" with value "${JSON.stringify(error.value, undefined, 2)}": "${error.message}"`)
42
42
  .join("\n")}`, options);
43
43
  this.name = "ModuleSettingsAreInvalidError";
44
44
  }
@@ -64,7 +64,7 @@ export const resolvePlugins = async (args) => {
64
64
  if (typeof plugin.loadMessages === "function") {
65
65
  result.data.loadMessages = (_args) => plugin.loadMessages({
66
66
  ..._args,
67
- nodeishFs: args.nodeishFs,
67
+ // renoved nodeishFs from args because we need to pass custom wrapped fs that establishes a watcher
68
68
  });
69
69
  }
70
70
  if (typeof plugin.saveMessages === "function") {
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
2
  import { describe, expect, it } from "vitest";
3
3
  import { resolvePlugins } from "./resolvePlugins.js";
4
- import { PluginLoadMessagesFunctionAlreadyDefinedError, PluginSaveMessagesFunctionAlreadyDefinedError, PluginHasInvalidIdError, PluginReturnedInvalidCustomApiError, PluginHasInvalidSchemaError, PluginsDoNotProvideLoadOrSaveMessagesError, } from "./errors.js";
4
+ import { PluginLoadMessagesFunctionAlreadyDefinedError, PluginSaveMessagesFunctionAlreadyDefinedError, PluginHasInvalidIdError, PluginReturnedInvalidCustomApiError, PluginsDoNotProvideLoadOrSaveMessagesError, } from "./errors.js";
5
5
  it("should return an error if a plugin uses an invalid id", async () => {
6
6
  const mockPlugin = {
7
7
  // @ts-expect-error - invalid id
@@ -18,25 +18,6 @@ it("should return an error if a plugin uses an invalid id", async () => {
18
18
  });
19
19
  expect(resolved.errors[0]).toBeInstanceOf(PluginHasInvalidIdError);
20
20
  });
21
- it("should return an error if a plugin uses APIs that are not available", async () => {
22
- const mockPlugin = {
23
- id: "plugin.namespace.undefinedApi",
24
- description: { en: "My plugin description" },
25
- displayName: { en: "My plugin" },
26
- // @ts-expect-error the key is not available in type
27
- nonExistentKey: {
28
- nonexistentOptions: "value",
29
- },
30
- loadMessages: () => undefined,
31
- saveMessages: () => undefined,
32
- };
33
- const resolved = await resolvePlugins({
34
- plugins: [mockPlugin],
35
- settings: {},
36
- nodeishFs: {},
37
- });
38
- expect(resolved.errors[0]).toBeInstanceOf(PluginHasInvalidSchemaError);
39
- });
40
21
  it("should expose the project settings including the plugin settings", async () => {
41
22
  const settings = {
42
23
  sourceLanguageTag: "en",
@@ -67,7 +48,7 @@ it("should expose the project settings including the plugin settings", async ()
67
48
  settings: settings,
68
49
  nodeishFs: {},
69
50
  });
70
- await resolved.data.loadMessages({ settings });
51
+ await resolved.data.loadMessages({ settings, nodeishFs: {} });
71
52
  await resolved.data.saveMessages({ settings, messages: [] });
72
53
  });
73
54
  describe("loadMessages", () => {
@@ -85,6 +66,7 @@ describe("loadMessages", () => {
85
66
  });
86
67
  expect(await resolved.data.loadMessages({
87
68
  settings: {},
69
+ nodeishFs: {},
88
70
  })).toEqual([{ id: "test", expressions: [], selectors: [], variants: [] }]);
89
71
  });
90
72
  it("should collect an error if function is defined twice in multiple plugins", async () => {
@@ -8,7 +8,7 @@ import type { ProjectSettings } from "@inlang/project-settings";
8
8
  *
9
9
  * - only uses minimally required functions to decrease the API footprint on the ecosystem.
10
10
  */
11
- export type NodeishFilesystemSubset = Pick<NodeishFilesystem, "readFile" | "readdir" | "mkdir" | "writeFile">;
11
+ export type NodeishFilesystemSubset = Pick<NodeishFilesystem, "readFile" | "readdir" | "mkdir" | "writeFile" | "watch">;
12
12
  /**
13
13
  * Function that resolves (imports and initializes) the plugins.
14
14
  */
@@ -26,6 +26,7 @@ export type ResolvePluginsFunction = (args: {
26
26
  export type ResolvedPluginApi = {
27
27
  loadMessages: (args: {
28
28
  settings: ProjectSettings;
29
+ nodeishFs: NodeishFilesystemSubset;
29
30
  }) => Promise<Message[]> | Message[];
30
31
  saveMessages: (args: {
31
32
  settings: ProjectSettings;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/resolve-modules/plugins/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,KAAK,EACX,mCAAmC,EACnC,6CAA6C,EAC7C,6CAA6C,EAC7C,uBAAuB,EACvB,2BAA2B,EAC3B,0CAA0C,EAC1C,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAE/D;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,IAAI,CACzC,iBAAiB,EACjB,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,CAC9C,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,EAAE;IAC3C,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IACtB,QAAQ,EAAE,eAAe,CAAA;IACzB,SAAS,EAAE,uBAAuB,CAAA;CAClC,KAAK,OAAO,CAAC;IACb,IAAI,EAAE,iBAAiB,CAAA;IACvB,MAAM,EAAE,KAAK,CACV,mCAAmC,GACnC,6CAA6C,GAC7C,6CAA6C,GAC7C,uBAAuB,GACvB,2BAA2B,GAC3B,0CAA0C,CAC5C,CAAA;CACD,CAAC,CAAA;AAEF;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC/B,YAAY,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,eAAe,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,CAAA;IACrF,YAAY,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,eAAe,CAAC;QAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAChG;;;;;;;;;;;;;;;;OAgBG;IACH,SAAS,EAAE,MAAM,CAAC,OAAO,MAAM,IAAI,MAAM,EAAE,GAAG,WAAW,MAAM,IAAI,MAAM,EAAE,EAAE,OAAO,CAAC,GAAG;QACvF,yBAAyB,CAAC,EAAE,2BAA2B,CAAA;KACvD,CAAA;CACD,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/resolve-modules/plugins/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,KAAK,EACX,mCAAmC,EACnC,6CAA6C,EAC7C,6CAA6C,EAC7C,uBAAuB,EACvB,2BAA2B,EAC3B,0CAA0C,EAC1C,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAE/D;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,IAAI,CACzC,iBAAiB,EACjB,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,OAAO,CACxD,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,EAAE;IAC3C,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IACtB,QAAQ,EAAE,eAAe,CAAA;IACzB,SAAS,EAAE,uBAAuB,CAAA;CAClC,KAAK,OAAO,CAAC;IACb,IAAI,EAAE,iBAAiB,CAAA;IACvB,MAAM,EAAE,KAAK,CACV,mCAAmC,GACnC,6CAA6C,GAC7C,6CAA6C,GAC7C,uBAAuB,GACvB,2BAA2B,GAC3B,0CAA0C,CAC5C,CAAA;CACD,CAAC,CAAA;AAEF;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC/B,YAAY,EAAE,CAAC,IAAI,EAAE;QACpB,QAAQ,EAAE,eAAe,CAAA;QACzB,SAAS,EAAE,uBAAuB,CAAA;KAClC,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,EAAE,CAAA;IACpC,YAAY,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,eAAe,CAAC;QAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAChG;;;;;;;;;;;;;;;;OAgBG;IACH,SAAS,EAAE,MAAM,CAAC,OAAO,MAAM,IAAI,MAAM,EAAE,GAAG,WAAW,MAAM,IAAI,MAAM,EAAE,EAAE,OAAO,CAAC,GAAG;QACvF,yBAAyB,CAAC,EAAE,2BAA2B,CAAA;KACvD,CAAA;CACD,CAAA"}
@@ -7,12 +7,16 @@ import { validatedModuleSettings } from "./validatedModuleSettings.js";
7
7
  const mockPluginSchema = Type.Object({
8
8
  pathPattern: Type.Union([
9
9
  Type.String({
10
- pattern: "^[^*]*\\{languageTag\\}[^*]*\\.json",
11
- description: "The PluginSettings must contain `{languageTag}` and end with `.json`.",
10
+ pattern: "^(\\./|\\../|/)[^*]*\\{languageTag\\}[^*]*\\.json",
11
+ description: "The pathPattern must contain `{languageTag}` and end with `.json`.",
12
12
  }),
13
- Type.Record(Type.String({}), Type.String({
14
- pattern: "^[^*]*\\{languageTag\\}[^*]*\\.json",
15
- description: "The PluginSettings must contain `{languageTag}` and end with `.json`.",
13
+ Type.Record(Type.String({
14
+ pattern: "^[^.]+$",
15
+ description: "Dots are not allowd ",
16
+ examples: ["website", "app", "homepage"],
17
+ }), Type.String({
18
+ pattern: "^(\\./|\\../|/)[^*]*\\{languageTag\\}[^*]*\\.json",
19
+ description: "The pathPattern must contain `{languageTag}` and end with `.json`.",
16
20
  })),
17
21
  ]),
18
22
  variableReferencePattern: Type.Array(Type.String()),
@@ -33,14 +37,19 @@ test("if PluginSchema does match with the moduleSettings", async () => {
33
37
  });
34
38
  expect(isValid).toBe("isValid");
35
39
  });
36
- test("if invalid module settings would pass", async () => {
40
+ test("if namespace settings are valide", async () => {
37
41
  const isValid = validatedModuleSettings({
38
42
  settingsSchema: mockPluginSchema,
39
43
  moduleSettings: {
40
- pathPattern: "./examples/example01/{languageTag}.json",
44
+ pathPattern: {
45
+ website: "./{languageTag}examplerFolder/ExampleFile.json",
46
+ app: "../{languageTag}examplerFolder/ExampleFile.json",
47
+ footer: "./{languageTag}examplerFolder/ExampleFile.json",
48
+ },
49
+ variableReferencePattern: ["{", "}"],
41
50
  },
42
51
  });
43
- expect(isValid).not.toBe("isValid");
52
+ expect(isValid).toBe("isValid");
44
53
  });
45
54
  test(" if MessageLintRuleSchema match with the settings", async () => {
46
55
  const isValid = await validatedModuleSettings({
@@ -1 +1 @@
1
- {"version":3,"file":"validatedModuleSettings.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/validatedModuleSettings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAG,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAEhE,eAAO,MAAM,uBAAuB,SAAU;IAC7C,cAAc,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,gBAAgB,CAAC,CAAA;IACzD,cAAc,EAAE,OAAO,CAAA;CACvB,KAAG,SAAS,GAAG,UAAU,EAWzB,CAAA"}
1
+ {"version":3,"file":"validatedModuleSettings.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/validatedModuleSettings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAEhE,eAAO,MAAM,uBAAuB,SAAU;IAC7C,cAAc,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,gBAAgB,CAAC,CAAA;IACzD,cAAc,EAAE,OAAO,CAAA;CACvB,KAAG,SAAS,GAAG,UAAU,EAWzB,CAAA"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inlang/sdk",
3
3
  "type": "module",
4
- "version": "0.16.0",
4
+ "version": "0.18.0",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -22,7 +22,7 @@
22
22
  "test": "tsc --noEmit && vitest run --passWithNoTests --coverage",
23
23
  "lint": "eslint ./src --fix",
24
24
  "format": "prettier ./src --write",
25
- "clean": "rm -rf ./dist ./.turbo ./node_modules"
25
+ "clean": "rm -rf ./dist ./node_modules"
26
26
  },
27
27
  "engines": {
28
28
  "node": ">=18.0.0"
@@ -28,6 +28,7 @@ it("intercepts paths correctly for readFile", async () => {
28
28
  readdir: vi.fn(),
29
29
  mkdir: vi.fn(),
30
30
  writeFile: vi.fn(),
31
+ watch: vi.fn(),
31
32
  } satisfies Record<keyof NodeishFilesystemSubset, any>
32
33
 
33
34
  const interceptedFs = createNodeishFsWithAbsolutePaths({
@@ -35,5 +35,9 @@ export const createNodeishFsWithAbsolutePaths = (args: {
35
35
  readdir: (path: string) => args.nodeishFs.readdir(makeAbsolute(path)),
36
36
  mkdir: (path: string) => args.nodeishFs.mkdir(makeAbsolute(path)),
37
37
  writeFile: (path: string, data: string) => args.nodeishFs.writeFile(makeAbsolute(path), data),
38
+ watch: (
39
+ path: string,
40
+ options: { signal: AbortSignal | undefined; recursive: boolean | undefined }
41
+ ) => args.nodeishFs.watch(makeAbsolute(path), options),
38
42
  }
39
43
  }
@@ -0,0 +1,40 @@
1
+ import { createNodeishMemoryFs } from "@lix-js/fs"
2
+ import { describe, it, expect } from "vitest"
3
+ import { createNodeishFsWithWatcher } from "./createNodeishFsWithWatcher.js"
4
+
5
+ describe("watcher", () => {
6
+ it("should trigger the update function when file changes", async () => {
7
+ let counter = 0
8
+ const fs = createNodeishFsWithWatcher({
9
+ nodeishFs: createNodeishMemoryFs(),
10
+ updateMessages: () => {
11
+ counter++
12
+ },
13
+ })
14
+
15
+ // establish watcher
16
+ await fs.writeFile("file.txt", "a")
17
+ await fs.readFile("file.txt", { encoding: "utf-8" })
18
+ expect(counter).toBe(0)
19
+
20
+ // initial file change
21
+ await fs.writeFile("file.txt", "b")
22
+ await new Promise((resolve) => setTimeout(resolve, 0))
23
+
24
+ expect(counter).toBe(1)
25
+
26
+ // change file
27
+ await fs.writeFile("file.txt", "a")
28
+ await new Promise((resolve) => setTimeout(resolve, 0))
29
+
30
+ //check if update function was called
31
+ expect(counter).toBe(2)
32
+
33
+ // change file
34
+ await fs.readFile("file.txt")
35
+ await new Promise((resolve) => setTimeout(resolve, 0))
36
+
37
+ //check if update function was called
38
+ expect(counter).toBe(2)
39
+ })
40
+ })
@@ -0,0 +1,56 @@
1
+ import type { NodeishFilesystemSubset } from "@inlang/plugin"
2
+
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 createNodeishFsWithWatcher = (args: {
10
+ nodeishFs: NodeishFilesystemSubset
11
+ updateMessages: () => void
12
+ }): NodeishFilesystemSubset => {
13
+ const pathList: string[] = []
14
+
15
+ const makeWatcher = (path: string) => {
16
+ const abortController = new AbortController()
17
+ ;(async () => {
18
+ try {
19
+ const watcher = args.nodeishFs.watch(path, {
20
+ signal: abortController.signal,
21
+ persistent: false,
22
+ })
23
+ //eslint-disable-next-line @typescript-eslint/no-unused-vars
24
+ for await (const event of watcher) {
25
+ args.updateMessages()
26
+ }
27
+ } catch (err: any) {
28
+ if (err.name === "AbortError") return
29
+ // https://github.com/inlang/monorepo/issues/1647
30
+ // the file does not exist (yet)
31
+ // this is not testable beacause the fs.watch api differs
32
+ // from node and lix. lenghty
33
+ else if (err.code === "ENOENT") return
34
+ throw err
35
+ }
36
+ })()
37
+ }
38
+
39
+ const readFileAndExtractPath = (path: string, options: { encoding: "utf-8" | "binary" }) => {
40
+ if (!pathList.includes(path)) {
41
+ makeWatcher(path)
42
+ pathList.push(path)
43
+ }
44
+ return args.nodeishFs.readFile(path, options)
45
+ }
46
+
47
+ return {
48
+ // @ts-expect-error
49
+ readFile: (path: string, options: { encoding: "utf-8" | "binary" }) =>
50
+ readFileAndExtractPath(path, options),
51
+ readdir: args.nodeishFs.readdir,
52
+ mkdir: args.nodeishFs.mkdir,
53
+ writeFile: args.nodeishFs.writeFile,
54
+ watch: args.nodeishFs.watch,
55
+ }
56
+ }
@@ -1,7 +1,13 @@
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 type { ProjectSettings, Plugin, MessageLintRule, Message } from "./versionedInterfaces.js"
4
+ import type {
5
+ ProjectSettings,
6
+ Plugin,
7
+ MessageLintRule,
8
+ Message,
9
+ NodeishFilesystemSubset,
10
+ } from "./versionedInterfaces.js"
5
11
  import type { ImportFunction } from "./resolve-modules/index.js"
6
12
  import type { InlangModule } from "@inlang/module"
7
13
  import {
@@ -10,7 +16,7 @@ import {
10
16
  ProjectSettingsFileNotFoundError,
11
17
  ProjectSettingsInvalidError,
12
18
  } from "./errors.js"
13
- import { createNodeishMemoryFs } from "@lix-js/fs"
19
+ import { createNodeishMemoryFs, normalizePath } from "@lix-js/fs"
14
20
  import { createMessage } from "./test-utilities/createMessage.js"
15
21
  import { tryCatch } from "@inlang/result"
16
22
 
@@ -826,4 +832,88 @@ describe("functionality", () => {
826
832
  project.query.messageLintReports.getAll.subscribe((r) => expect(r).toEqual([]))
827
833
  })
828
834
  })
835
+
836
+ describe("watcher", () => {
837
+ it("changing files in resources should trigger callback of message query", async () => {
838
+ const fs = createNodeishMemoryFs()
839
+
840
+ const messages = {
841
+ $schema: "https://inlang.com/schema/inlang-message-format",
842
+ data: [
843
+ {
844
+ id: "test",
845
+ selectors: [],
846
+ variants: [
847
+ {
848
+ match: [],
849
+ languageTag: "en",
850
+ pattern: [
851
+ {
852
+ type: "Text",
853
+ value: "test",
854
+ },
855
+ ],
856
+ },
857
+ ],
858
+ },
859
+ ],
860
+ }
861
+
862
+ await fs.writeFile("./messages.json", JSON.stringify(messages))
863
+
864
+ const getMessages = async (customFs: NodeishFilesystemSubset) => {
865
+ const file = await customFs.readFile("./messages.json", { encoding: "utf-8" })
866
+ return JSON.parse(file.toString()).data
867
+ }
868
+
869
+ const mockMessageFormatPlugin: Plugin = {
870
+ id: "plugin.inlang.messageFormat",
871
+ description: { en: "Mock plugin description" },
872
+ displayName: { en: "Mock Plugin" },
873
+
874
+ loadMessages: async (args) => await getMessages(args.nodeishFs),
875
+ saveMessages: () => undefined as any,
876
+ }
877
+
878
+ const settings: ProjectSettings = {
879
+ sourceLanguageTag: "en",
880
+ languageTags: ["en"],
881
+ modules: ["plugin.js"],
882
+ "plugin.inlang.messageFormat": {
883
+ filePath: "./messages.json",
884
+ },
885
+ }
886
+
887
+ await fs.writeFile("./project.inlang.json", JSON.stringify(settings))
888
+
889
+ // establish watcher
890
+ const project = await loadProject({
891
+ settingsFilePath: normalizePath("/project.inlang.json"),
892
+ nodeishFs: fs,
893
+ _import: async () => ({
894
+ default: mockMessageFormatPlugin,
895
+ }),
896
+ })
897
+
898
+ let counter = 0
899
+
900
+ project.query.messages.getAll.subscribe(() => {
901
+ counter = counter + 1
902
+ })
903
+
904
+ expect(counter).toBe(1)
905
+
906
+ // change file
907
+ await fs.writeFile("./messages.json", JSON.stringify(messages))
908
+ await new Promise((resolve) => setTimeout(resolve, 0))
909
+
910
+ expect(counter).toBe(2)
911
+
912
+ // change file
913
+ await fs.writeFile("./messages.json", JSON.stringify(messages))
914
+ await new Promise((resolve) => setTimeout(resolve, 0))
915
+
916
+ expect(counter).toBe(3)
917
+ })
918
+ })
829
919
  })
@@ -25,6 +25,7 @@ import { migrateIfOutdated } from "@inlang/project-settings/migration"
25
25
  import { createNodeishFsWithAbsolutePaths } from "./createNodeishFsWithAbsolutePaths.js"
26
26
  import { normalizePath } from "@lix-js/fs"
27
27
  import { isAbsolutePath } from "./isAbsolutePath.js"
28
+ import { createNodeishFsWithWatcher } from "./createNodeishFsWithWatcher.js"
28
29
 
29
30
  const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
30
31
 
@@ -126,6 +127,7 @@ export const loadProject = async (args: {
126
127
  createEffect(() => (settingsValue = settings()!)) // workaround to not run effects twice (e.g. settings change + modules change) (I'm sure there exists a solid way of doing this, but I haven't found it yet)
127
128
 
128
129
  const [messages, setMessages] = createSignal<Message[]>()
130
+
129
131
  createEffect(() => {
130
132
  const conf = settings()
131
133
  if (!conf) return
@@ -138,16 +140,28 @@ export const loadProject = async (args: {
138
140
  return
139
141
  }
140
142
 
141
- makeTrulyAsync(
142
- _resolvedModules.resolvedPluginApi.loadMessages({
143
- settings: settingsValue,
144
- })
145
- )
146
- .then((messages) => {
147
- setMessages(messages)
148
- markInitAsComplete()
149
- })
150
- .catch((err) => markInitAsFailed(new PluginLoadMessagesError({ cause: err })))
143
+ const loadAndSetMessages = async (fs: NodeishFilesystemSubset) => {
144
+ makeTrulyAsync(
145
+ _resolvedModules.resolvedPluginApi.loadMessages({
146
+ settings: settingsValue,
147
+ nodeishFs: fs,
148
+ })
149
+ )
150
+ .then((messages) => {
151
+ setMessages(messages)
152
+ markInitAsComplete()
153
+ })
154
+ .catch((err) => markInitAsFailed(new PluginLoadMessagesError({ cause: err })))
155
+ }
156
+
157
+ const fsWithWatcher = createNodeishFsWithWatcher({
158
+ nodeishFs: nodeishFs,
159
+ updateMessages: () => {
160
+ loadAndSetMessages(nodeishFs)
161
+ },
162
+ })
163
+
164
+ loadAndSetMessages(fsWithWatcher)
151
165
  })
152
166
 
153
167
  // -- installed items ----------------------------------------------------
@@ -198,21 +212,23 @@ export const loadProject = async (args: {
198
212
  500,
199
213
  async (newMessages) => {
200
214
  try {
201
- await resolvedModules()?.resolvedPluginApi.saveMessages({
202
- settings: settingsValue,
203
- messages: newMessages,
204
- })
215
+ if (JSON.stringify(newMessages) !== JSON.stringify(messages())) {
216
+ await resolvedModules()?.resolvedPluginApi.saveMessages({
217
+ settings: settingsValue,
218
+ messages: newMessages,
219
+ })
220
+ }
205
221
  } catch (err) {
206
222
  throw new PluginSaveMessagesError({
207
223
  cause: err,
208
224
  })
209
225
  }
210
- if (
211
- newMessages.length !== 0 &&
212
- JSON.stringify(newMessages) !== JSON.stringify(messages())
213
- ) {
214
- setMessages(newMessages)
215
- }
226
+ // if (
227
+ // newMessages.length !== 0 &&
228
+ // JSON.stringify(newMessages) !== JSON.stringify(messages())
229
+ // ) {
230
+ // setMessages(newMessages)
231
+ // }
216
232
  },
217
233
  { atBegin: false }
218
234
  )
@@ -40,7 +40,10 @@ export class ModuleExportIsInvalidError extends ModuleError {
40
40
  constructor(options: { module: string; errors: ValueError[] }) {
41
41
  super(
42
42
  `The export(s) of "${options.module}" are invalid:\n\n${options.errors
43
- .map((error) => `"${error.path}" "${error.value}": "${error.message}"`)
43
+ .map(
44
+ (error) =>
45
+ `"${error.path}" "${JSON.stringify(error.value, undefined, 2)}": "${error.message}"`
46
+ )
44
47
  .join("\n")}`,
45
48
  options
46
49
  )
@@ -51,8 +54,13 @@ export class ModuleExportIsInvalidError extends ModuleError {
51
54
  export class ModuleSettingsAreInvalidError extends ModuleError {
52
55
  constructor(options: { module: string; errors: ValueError[] }) {
53
56
  super(
54
- `The settings are invalid of "${module}" are invalid:\n\n${options.errors
55
- .map((error) => `Path "${error.path}" with value "${error.value}": "${error.message}"`)
57
+ `The settings are invalid of "${options.module}" are invalid:\n\n${options.errors
58
+ .map(
59
+ (error) =>
60
+ `Path "${error.path}" with value "${JSON.stringify(error.value, undefined, 2)}": "${
61
+ error.message
62
+ }"`
63
+ )
56
64
  .join("\n")}`,
57
65
  options
58
66
  )
@@ -6,7 +6,6 @@ import {
6
6
  PluginSaveMessagesFunctionAlreadyDefinedError,
7
7
  PluginHasInvalidIdError,
8
8
  PluginReturnedInvalidCustomApiError,
9
- PluginHasInvalidSchemaError,
10
9
  PluginsDoNotProvideLoadOrSaveMessagesError,
11
10
  } from "./errors.js"
12
11
  import type { Plugin } from "@inlang/plugin"
@@ -31,28 +30,6 @@ it("should return an error if a plugin uses an invalid id", async () => {
31
30
  expect(resolved.errors[0]).toBeInstanceOf(PluginHasInvalidIdError)
32
31
  })
33
32
 
34
- it("should return an error if a plugin uses APIs that are not available", async () => {
35
- const mockPlugin: Plugin = {
36
- id: "plugin.namespace.undefinedApi",
37
- description: { en: "My plugin description" },
38
- displayName: { en: "My plugin" },
39
- // @ts-expect-error the key is not available in type
40
- nonExistentKey: {
41
- nonexistentOptions: "value",
42
- },
43
- loadMessages: () => undefined as any,
44
- saveMessages: () => undefined as any,
45
- }
46
-
47
- const resolved = await resolvePlugins({
48
- plugins: [mockPlugin],
49
- settings: {} as any,
50
- nodeishFs: {} as any,
51
- })
52
-
53
- expect(resolved.errors[0]).toBeInstanceOf(PluginHasInvalidSchemaError)
54
- })
55
-
56
33
  it("should expose the project settings including the plugin settings", async () => {
57
34
  const settings: ProjectSettings = {
58
35
  sourceLanguageTag: "en",
@@ -83,7 +60,7 @@ it("should expose the project settings including the plugin settings", async ()
83
60
  settings: settings,
84
61
  nodeishFs: {} as any,
85
62
  })
86
- await resolved.data.loadMessages!({ settings })
63
+ await resolved.data.loadMessages!({ settings, nodeishFs: {} as any })
87
64
  await resolved.data.saveMessages!({ settings, messages: [] })
88
65
  })
89
66
 
@@ -105,6 +82,7 @@ describe("loadMessages", () => {
105
82
  expect(
106
83
  await resolved.data.loadMessages!({
107
84
  settings: {} as any,
85
+ nodeishFs: {} as any,
108
86
  })
109
87
  ).toEqual([{ id: "test", expressions: [], selectors: [], variants: [] }])
110
88
  })
@@ -91,7 +91,7 @@ export const resolvePlugins: ResolvePluginsFunction = async (args) => {
91
91
  result.data.loadMessages = (_args) =>
92
92
  plugin.loadMessages!({
93
93
  ..._args,
94
- nodeishFs: args.nodeishFs,
94
+ // renoved nodeishFs from args because we need to pass custom wrapped fs that establishes a watcher
95
95
  })
96
96
  }
97
97
 
@@ -18,7 +18,7 @@ import type { ProjectSettings } from "@inlang/project-settings"
18
18
  */
19
19
  export type NodeishFilesystemSubset = Pick<
20
20
  NodeishFilesystem,
21
- "readFile" | "readdir" | "mkdir" | "writeFile"
21
+ "readFile" | "readdir" | "mkdir" | "writeFile" | "watch"
22
22
  >
23
23
 
24
24
  /**
@@ -44,7 +44,10 @@ export type ResolvePluginsFunction = (args: {
44
44
  * The API after resolving the plugins.
45
45
  */
46
46
  export type ResolvedPluginApi = {
47
- loadMessages: (args: { settings: ProjectSettings }) => Promise<Message[]> | Message[]
47
+ loadMessages: (args: {
48
+ settings: ProjectSettings
49
+ nodeishFs: NodeishFilesystemSubset
50
+ }) => Promise<Message[]> | Message[]
48
51
  saveMessages: (args: { settings: ProjectSettings; messages: Message[] }) => Promise<void> | void
49
52
  /**
50
53
  * App specific APIs.
@@ -8,14 +8,18 @@ import { validatedModuleSettings } from "./validatedModuleSettings.js"
8
8
  const mockPluginSchema: Plugin["settingsSchema"] = Type.Object({
9
9
  pathPattern: Type.Union([
10
10
  Type.String({
11
- pattern: "^[^*]*\\{languageTag\\}[^*]*\\.json",
12
- description: "The PluginSettings must contain `{languageTag}` and end with `.json`.",
11
+ pattern: "^(\\./|\\../|/)[^*]*\\{languageTag\\}[^*]*\\.json",
12
+ description: "The pathPattern must contain `{languageTag}` and end with `.json`.",
13
13
  }),
14
14
  Type.Record(
15
- Type.String({}),
16
15
  Type.String({
17
- pattern: "^[^*]*\\{languageTag\\}[^*]*\\.json",
18
- description: "The PluginSettings must contain `{languageTag}` and end with `.json`.",
16
+ pattern: "^[^.]+$",
17
+ description: "Dots are not allowd ",
18
+ examples: ["website", "app", "homepage"],
19
+ }),
20
+ Type.String({
21
+ pattern: "^(\\./|\\../|/)[^*]*\\{languageTag\\}[^*]*\\.json",
22
+ description: "The pathPattern must contain `{languageTag}` and end with `.json`.",
19
23
  })
20
24
  ),
21
25
  ]),
@@ -43,14 +47,19 @@ test("if PluginSchema does match with the moduleSettings", async () => {
43
47
  expect(isValid).toBe("isValid")
44
48
  })
45
49
 
46
- test("if invalid module settings would pass", async () => {
50
+ test("if namespace settings are valide", async () => {
47
51
  const isValid = validatedModuleSettings({
48
52
  settingsSchema: mockPluginSchema,
49
53
  moduleSettings: {
50
- pathPattern: "./examples/example01/{languageTag}.json",
54
+ pathPattern: {
55
+ website: "./{languageTag}examplerFolder/ExampleFile.json",
56
+ app: "../{languageTag}examplerFolder/ExampleFile.json",
57
+ footer: "./{languageTag}examplerFolder/ExampleFile.json",
58
+ },
59
+ variableReferencePattern: ["{", "}"],
51
60
  },
52
61
  })
53
- expect(isValid).not.toBe("isValid")
62
+ expect(isValid).toBe("isValid")
54
63
  })
55
64
 
56
65
  test(" if MessageLintRuleSchema match with the settings", async () => {
@@ -1,4 +1,4 @@
1
- import type { InlangModule } from "@inlang/module"
1
+ import type { InlangModule } from "@inlang/module"
2
2
  import type { TSchema } from "@sinclair/typebox"
3
3
  import { Value, type ValueError } from "@sinclair/typebox/value"
4
4