@inlang/sdk 0.17.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.
- package/dist/createNodeishFsWithAbsolutePaths.d.ts.map +1 -1
- package/dist/createNodeishFsWithAbsolutePaths.js +1 -0
- package/dist/createNodeishFsWithAbsolutePaths.test.js +1 -0
- package/dist/createNodeishFsWithWatcher.d.ts +12 -0
- package/dist/createNodeishFsWithWatcher.d.ts.map +1 -0
- package/dist/createNodeishFsWithWatcher.js +50 -0
- package/dist/createNodeishFsWithWatcher.test.d.ts +2 -0
- package/dist/createNodeishFsWithWatcher.test.d.ts.map +1 -0
- package/dist/createNodeishFsWithWatcher.test.js +32 -0
- package/dist/loadProject.d.ts.map +1 -1
- package/dist/loadProject.js +31 -16
- package/dist/loadProject.test.js +69 -1
- package/dist/resolve-modules/plugins/resolvePlugins.js +1 -1
- package/dist/resolve-modules/plugins/resolvePlugins.test.js +2 -1
- package/dist/resolve-modules/plugins/types.d.ts +2 -1
- package/dist/resolve-modules/plugins/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/createNodeishFsWithAbsolutePaths.test.ts +1 -0
- package/src/createNodeishFsWithAbsolutePaths.ts +4 -0
- package/src/createNodeishFsWithWatcher.test.ts +40 -0
- package/src/createNodeishFsWithWatcher.ts +56 -0
- package/src/loadProject.test.ts +92 -2
- package/src/loadProject.ts +36 -20
- package/src/resolve-modules/plugins/resolvePlugins.test.ts +2 -1
- package/src/resolve-modules/plugins/resolvePlugins.ts +1 -1
- package/src/resolve-modules/plugins/types.ts +5 -2
|
@@ -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,
|
|
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
|
};
|
|
@@ -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 @@
|
|
|
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;
|
|
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"}
|
package/dist/loadProject.js
CHANGED
|
@@ -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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 (
|
|
149
|
-
|
|
150
|
-
|
|
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());
|
package/dist/loadProject.test.js
CHANGED
|
@@ -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
|
});
|
|
@@ -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
|
|
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") {
|
|
@@ -48,7 +48,7 @@ it("should expose the project settings including the plugin settings", async ()
|
|
|
48
48
|
settings: settings,
|
|
49
49
|
nodeishFs: {},
|
|
50
50
|
});
|
|
51
|
-
await resolved.data.loadMessages({ settings });
|
|
51
|
+
await resolved.data.loadMessages({ settings, nodeishFs: {} });
|
|
52
52
|
await resolved.data.saveMessages({ settings, messages: [] });
|
|
53
53
|
});
|
|
54
54
|
describe("loadMessages", () => {
|
|
@@ -66,6 +66,7 @@ describe("loadMessages", () => {
|
|
|
66
66
|
});
|
|
67
67
|
expect(await resolved.data.loadMessages({
|
|
68
68
|
settings: {},
|
|
69
|
+
nodeishFs: {},
|
|
69
70
|
})).toEqual([{ id: "test", expressions: [], selectors: [], variants: [] }]);
|
|
70
71
|
});
|
|
71
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,
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inlang/sdk",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "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
|
|
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
|
+
}
|
package/src/loadProject.test.ts
CHANGED
|
@@ -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 {
|
|
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
|
})
|
package/src/loadProject.ts
CHANGED
|
@@ -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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
) {
|
|
214
|
-
|
|
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
|
)
|
|
@@ -60,7 +60,7 @@ it("should expose the project settings including the plugin settings", async ()
|
|
|
60
60
|
settings: settings,
|
|
61
61
|
nodeishFs: {} as any,
|
|
62
62
|
})
|
|
63
|
-
await resolved.data.loadMessages!({ settings })
|
|
63
|
+
await resolved.data.loadMessages!({ settings, nodeishFs: {} as any })
|
|
64
64
|
await resolved.data.saveMessages!({ settings, messages: [] })
|
|
65
65
|
})
|
|
66
66
|
|
|
@@ -82,6 +82,7 @@ describe("loadMessages", () => {
|
|
|
82
82
|
expect(
|
|
83
83
|
await resolved.data.loadMessages!({
|
|
84
84
|
settings: {} as any,
|
|
85
|
+
nodeishFs: {} as any,
|
|
85
86
|
})
|
|
86
87
|
).toEqual([{ id: "test", expressions: [], selectors: [], variants: [] }])
|
|
87
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
|
|
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: {
|
|
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.
|