@inlang/sdk 0.34.1 → 0.34.3

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.
@@ -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,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;CAU7C;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;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAC9B,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAA;KAAE;CAOtF;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAC9B,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAA;KAAE;CAOtF"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,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;CAW7C;AAYD,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;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAC9B,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAA;KAAE;CAOtF;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAC9B,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAA;KAAE;CAOtF"}
package/dist/errors.js CHANGED
@@ -7,13 +7,23 @@ export class LoadProjectInvalidArgument extends Error {
7
7
  export class ProjectSettingsInvalidError extends Error {
8
8
  constructor(options) {
9
9
  // TODO: beatufiy ValueErrors
10
- super(`The project settings are invalid:\n\n${options.errors
10
+ super(`The project settings are invalid:
11
+ ${options.errors
11
12
  .filter((error) => error.path)
12
- .map((error) => `"${error.path}":\n\n${error.message}`)
13
+ .map(FormatProjectSettingsError)
13
14
  .join("\n")}`);
14
15
  this.name = "ProjectSettingsInvalidError";
15
16
  }
16
17
  }
18
+ function FormatProjectSettingsError(error) {
19
+ let msg = `${error.message} at ${error.path}`;
20
+ if (error.path.startsWith("/modules/")) {
21
+ msg += `
22
+ value = "${error.value}"
23
+ - ${error.schema.allOf.map((o) => `${o.description ?? ""}`).join("\n- ")}`;
24
+ }
25
+ return msg;
26
+ }
17
27
  export class ProjectSettingsFileJSONSyntaxError extends Error {
18
28
  constructor(options) {
19
29
  super(`The settings file at "${options.path}" is not a valid JSON file:\n\n${options.cause}`, options);
@@ -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;AAwBhF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAiChD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACvC,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,cAAc,CAAA;CACxB,GAAG,OAAO,CAAC,aAAa,CAAC,CA2XzB;AAsHD,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;AAwBhF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAmChD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACvC,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,cAAc,CAAA;CACxB,GAAG,OAAO,CAAC,aAAa,CAAC,CAuXzB;AAsHD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAQtE"}
@@ -45,6 +45,7 @@ export async function loadProject(args) {
45
45
  // won't even be loaded. do not throw anywhere else. otherwise, apps
46
46
  // can't handle errors gracefully.
47
47
  assertValidProjectPath(projectPath);
48
+ debug(projectPath);
48
49
  const nodeishFs = createNodeishFsWithAbsolutePaths({
49
50
  projectPath,
50
51
  nodeishFs: args.repo.nodeishFs,
@@ -79,6 +80,11 @@ export async function loadProject(args) {
79
80
  const setSettings = (settings) => {
80
81
  try {
81
82
  const validatedSettings = parseSettings(settings);
83
+ if (validatedSettings.experimental?.persistence) {
84
+ settings["plugin.sdk.persistence"] = {
85
+ pathPattern: projectPath + "/messages.json",
86
+ };
87
+ }
82
88
  _setSettings(validatedSettings);
83
89
  writeSettingsToDisk(validatedSettings);
84
90
  return { data: undefined };
@@ -118,15 +124,14 @@ export async function loadProject(args) {
118
124
  const _resolvedModules = resolvedModules();
119
125
  if (!_resolvedModules)
120
126
  return;
121
- if (!_resolvedModules.resolvedPluginApi.loadMessages) {
127
+ const resolvedPluginApi = _resolvedModules.resolvedPluginApi;
128
+ if (!resolvedPluginApi.loadMessages) {
122
129
  markInitAsFailed(undefined);
123
130
  return;
124
131
  }
125
132
  const _settings = settings();
126
133
  if (!_settings)
127
134
  return;
128
- // get plugin finding the plugin that provides loadMessages function
129
- const loadMessagePlugin = _resolvedModules.plugins.find((plugin) => plugin.loadMessages !== undefined);
130
135
  // TODO #1844 this watcher needs to get pruned when we have a change in the configs which will trigger this again
131
136
  const fsWithWatcher = createNodeishFsWithWatcher({
132
137
  nodeishFs: nodeishFs,
@@ -136,7 +141,7 @@ export async function loadProject(args) {
136
141
  // preserving console.logs as comments pending #
137
142
  debug("load messages because of a change in the message.json files");
138
143
  loadMessagesViaPlugin(fsWithWatcher, messageLockDirPath, messageStates, messagesQuery, settings(), // NOTE we bang here - we don't expect the settings to become null during the livetime of a project
139
- loadMessagePlugin)
144
+ resolvedPluginApi)
140
145
  .catch((e) => setLoadMessagesViaPluginError(new PluginLoadMessagesError({ cause: e })))
141
146
  .then(() => {
142
147
  if (loadMessagesViaPluginError() !== undefined) {
@@ -145,7 +150,7 @@ export async function loadProject(args) {
145
150
  });
146
151
  },
147
152
  });
148
- loadMessagesViaPlugin(fsWithWatcher, messageLockDirPath, messageStates, messagesQuery, _settings, loadMessagePlugin)
153
+ loadMessagesViaPlugin(fsWithWatcher, messageLockDirPath, messageStates, messagesQuery, _settings, resolvedPluginApi)
149
154
  .then(() => {
150
155
  markInitAsComplete();
151
156
  })
@@ -193,10 +198,9 @@ export async function loadProject(args) {
193
198
  const _resolvedModules = resolvedModules();
194
199
  if (!_resolvedModules)
195
200
  return;
201
+ const resolvedPluginApi = _resolvedModules.resolvedPluginApi;
196
202
  const currentMessageIds = new Set(messagesQuery.includedMessageIds());
197
203
  const deletedTrackedMessages = [...trackedMessages].filter((tracked) => !currentMessageIds.has(tracked[0]));
198
- const saveMessagesPlugin = _resolvedModules.plugins.find((plugin) => plugin.saveMessages !== undefined);
199
- const loadMessagesPlugin = _resolvedModules.plugins.find((plugin) => plugin.loadMessages !== undefined);
200
204
  for (const messageId of currentMessageIds) {
201
205
  if (!trackedMessages.has(messageId)) {
202
206
  // we create a new root to be able to cleanup an effect for a message that got deleted
@@ -213,8 +217,9 @@ export async function loadProject(args) {
213
217
  }
214
218
  // don't trigger saves or set dirty flags during initial setup
215
219
  if (!initialSetup) {
220
+ debug("message changed", messageId);
216
221
  messageStates.messageDirtyFlags[message.id] = true;
217
- saveMessagesViaPlugin(nodeishFs, messageLockDirPath, messageStates, messagesQuery, settings(), saveMessagesPlugin, loadMessagesPlugin)
222
+ saveMessagesViaPlugin(nodeishFs, messageLockDirPath, messageStates, messagesQuery, settings(), resolvedPluginApi)
218
223
  .catch((e) => setSaveMessagesViaPluginError(new PluginSaveMessagesError({ cause: e })))
219
224
  .then(() => {
220
225
  if (saveMessagesViaPluginError() !== undefined) {
@@ -239,7 +244,7 @@ export async function loadProject(args) {
239
244
  }
240
245
  if (deletedTrackedMessages.length > 0) {
241
246
  // we keep track of the latest save within the loadProject call to await it at the end - this is not used in subsequetial upserts
242
- saveMessagesViaPlugin(nodeishFs, messageLockDirPath, messageStates, messagesQuery, settings(), saveMessagesPlugin, loadMessagesPlugin)
247
+ saveMessagesViaPlugin(nodeishFs, messageLockDirPath, messageStates, messagesQuery, settings(), resolvedPluginApi)
243
248
  .catch((e) => setSaveMessagesViaPluginError(new PluginSaveMessagesError({ cause: e })))
244
249
  .then(() => {
245
250
  if (saveMessagesViaPluginError() !== undefined) {
@@ -408,7 +413,7 @@ export function createSubscribable(signal) {
408
413
  * @param loadPlugin
409
414
  * @returns void - updates the files and messages in of the project in place
410
415
  */
411
- async function loadMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuery, settingsValue, loadPlugin) {
416
+ async function loadMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuery, settingsValue, resolvedPluginApi) {
412
417
  const experimentalAliases = !!settingsValue.experimental?.aliases;
413
418
  // loading is an asynchronous process - check if another load is in progress - queue this call if so
414
419
  if (messageState.isLoading) {
@@ -423,7 +428,7 @@ async function loadMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuer
423
428
  let lockTime = undefined;
424
429
  try {
425
430
  lockTime = await acquireFileLock(fs, lockDirPath, "loadMessage");
426
- const loadedMessages = await makeTrulyAsync(loadPlugin.loadMessages({
431
+ const loadedMessages = await makeTrulyAsync(resolvedPluginApi.loadMessages({
427
432
  settings: settingsValue,
428
433
  nodeishFs: fs,
429
434
  }));
@@ -506,7 +511,7 @@ async function loadMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuer
506
511
  // reset sheduling to except scheduling again
507
512
  messageState.sheduledLoadMessagesViaPlugin = undefined;
508
513
  // recall load unawaited to allow stack to pop
509
- loadMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuery, settingsValue, loadPlugin)
514
+ loadMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuery, settingsValue, resolvedPluginApi)
510
515
  .then(() => {
511
516
  // resolve the scheduled load message promise
512
517
  executingScheduledMessages[1]();
@@ -517,7 +522,7 @@ async function loadMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuer
517
522
  });
518
523
  }
519
524
  }
520
- async function saveMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuery, settingsValue, savePlugin, loadPlugin) {
525
+ async function saveMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuery, settingsValue, resolvedPluginApi) {
521
526
  // queue next save if we have a save ongoing
522
527
  if (messageState.isSaving) {
523
528
  if (!messageState.sheduledSaveMessages) {
@@ -566,7 +571,7 @@ async function saveMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuer
566
571
  messageDirtyFlagsBeforeSave = { ...messageState.messageDirtyFlags };
567
572
  messageState.messageDirtyFlags = {};
568
573
  // NOTE: this assumes that the plugin will handle message ordering
569
- await savePlugin.saveMessages({
574
+ await resolvedPluginApi.saveMessages({
570
575
  settings: settingsValue,
571
576
  messages: messagesToExport,
572
577
  nodeishFs: fs,
@@ -581,7 +586,7 @@ async function saveMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuer
581
586
  // if there is a queued load, allow it to take the lock before we run additional saves.
582
587
  if (messageState.sheduledLoadMessagesViaPlugin) {
583
588
  debug("saveMessagesViaPlugin calling queued loadMessagesViaPlugin to share lock");
584
- await loadMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuery, settingsValue, loadPlugin);
589
+ await loadMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuery, settingsValue, resolvedPluginApi);
585
590
  }
586
591
  messageState.isSaving = false;
587
592
  }
@@ -614,7 +619,7 @@ async function saveMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuer
614
619
  if (messageState.sheduledSaveMessages) {
615
620
  const executingSheduledSaveMessages = messageState.sheduledSaveMessages;
616
621
  messageState.sheduledSaveMessages = undefined;
617
- saveMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuery, settingsValue, savePlugin, loadPlugin)
622
+ saveMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuery, settingsValue, resolvedPluginApi)
618
623
  .then(() => {
619
624
  executingSheduledSaveMessages[1]();
620
625
  })
@@ -0,0 +1,31 @@
1
+ import type { ProjectSettings, Message } from "@inlang/sdk";
2
+ import { type NodeishFilesystem } from "@lix-js/fs";
3
+ export declare const pluginId = "plugin.sdk.persistence";
4
+ export declare function loadMessages(args: {
5
+ settings: ProjectSettings;
6
+ nodeishFs: NodeishFilesystem;
7
+ }): Promise<{
8
+ id: string;
9
+ alias: Record<string, string>;
10
+ selectors: {
11
+ type: "VariableReference";
12
+ name: string;
13
+ }[];
14
+ variants: {
15
+ languageTag: string;
16
+ match: string[];
17
+ pattern: ({
18
+ type: "Text";
19
+ value: string;
20
+ } | {
21
+ type: "VariableReference";
22
+ name: string;
23
+ })[];
24
+ }[];
25
+ }[]>;
26
+ export declare function saveMessages(args: {
27
+ settings: ProjectSettings;
28
+ nodeishFs: NodeishFilesystem;
29
+ messages: Message[];
30
+ }): Promise<void>;
31
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/persistence/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAC3D,OAAO,EAAc,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAM/D,eAAO,MAAM,QAAQ,2BAA2B,CAAA;AAEhD,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACxC,QAAQ,EAAE,eAAe,CAAA;IACzB,SAAS,EAAE,iBAAiB,CAAA;CAC5B;;;;;;;;;;;;;;;;;;KAeA;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACxC,QAAQ,EAAE,eAAe,CAAA;IACzB,SAAS,EAAE,iBAAiB,CAAA;IAC5B,QAAQ,EAAE,OAAO,EAAE,CAAA;CACnB,iBAcA"}
@@ -0,0 +1,42 @@
1
+ import { getDirname } from "@lix-js/fs";
2
+ import { normalizeMessage } from "../storage/helper.js";
3
+ import _debug from "debug";
4
+ const debug = _debug("sdk:persistence");
5
+ export const pluginId = "plugin.sdk.persistence";
6
+ export async function loadMessages(args) {
7
+ let result = [];
8
+ const pathPattern = args.settings[pluginId]?.pathPattern;
9
+ debug("loadMessages", pathPattern);
10
+ try {
11
+ const file = await args.nodeishFs.readFile(pathPattern, { encoding: "utf-8" });
12
+ result = JSON.parse(file);
13
+ }
14
+ catch (error) {
15
+ if (error?.code !== "ENOENT") {
16
+ debug("loadMessages", error);
17
+ throw error;
18
+ }
19
+ }
20
+ return result;
21
+ }
22
+ export async function saveMessages(args) {
23
+ const pathPattern = args.settings[pluginId]?.pathPattern;
24
+ debug("saveMessages", pathPattern);
25
+ try {
26
+ await createDirectoryIfNotExits(getDirname(pathPattern), args.nodeishFs);
27
+ await args.nodeishFs.writeFile(pathPattern,
28
+ // 2 spaces indentation
29
+ JSON.stringify(args.messages.map(normalizeMessage), undefined, 2));
30
+ }
31
+ catch (error) {
32
+ debug("saveMessages", error);
33
+ }
34
+ }
35
+ async function createDirectoryIfNotExits(path, nodeishFs) {
36
+ try {
37
+ await nodeishFs.mkdir(path, { recursive: true });
38
+ }
39
+ catch {
40
+ // assume that the directory already exists
41
+ }
42
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=plugin.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.test.d.ts","sourceRoot":"","sources":["../../src/persistence/plugin.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,49 @@
1
+ import { test, expect } from "vitest";
2
+ import { createMessage, createNodeishMemoryFs } from "../test-utilities/index.js";
3
+ import { normalizeMessage } from "../storage/helper.js";
4
+ import { pluginId } from "./plugin.js";
5
+ const mockMessages = [
6
+ createMessage("first_message", {
7
+ en: "If this fails I will be sad",
8
+ }),
9
+ createMessage("second_message", {
10
+ en: "Let's see if this works",
11
+ de: "Mal sehen ob das funktioniert",
12
+ }),
13
+ ];
14
+ // the test ensures:
15
+ // - messages can be loaded
16
+ // - messages can be saved
17
+ // - after loading and saving messages, the state is the same as before (roundtrip)
18
+ test("roundtrip (saving/loading messages)", async () => {
19
+ const { loadMessages, saveMessages } = await import("./plugin.js");
20
+ const fs = createNodeishMemoryFs();
21
+ const projectDir = "/test/project.inlang";
22
+ const pathPattern = projectDir + "/messages.json";
23
+ const persistedMessages = JSON.stringify(mockMessages.map(normalizeMessage), undefined, 2);
24
+ const settings = {
25
+ sourceLanguageTag: "en",
26
+ languageTags: ["en", "de"],
27
+ modules: [],
28
+ [pluginId]: { pathPattern },
29
+ };
30
+ await fs.mkdir(projectDir, { recursive: true });
31
+ await fs.writeFile(pathPattern, persistedMessages);
32
+ const firstMessageLoad = await loadMessages({
33
+ settings,
34
+ nodeishFs: fs,
35
+ });
36
+ expect(firstMessageLoad).toStrictEqual(mockMessages);
37
+ await saveMessages({
38
+ settings,
39
+ nodeishFs: fs,
40
+ messages: firstMessageLoad,
41
+ });
42
+ const afterRoundtrip = await fs.readFile(pathPattern, { encoding: "utf-8" });
43
+ expect(afterRoundtrip).toStrictEqual(persistedMessages);
44
+ const messagesAfterRoundtrip = await loadMessages({
45
+ settings,
46
+ nodeishFs: fs,
47
+ });
48
+ expect(messagesAfterRoundtrip).toStrictEqual(firstMessageLoad);
49
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"resolvePlugins.d.ts","sourceRoot":"","sources":["../../../src/resolve-modules/plugins/resolvePlugins.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAiBxD,eAAO,MAAM,cAAc,EAAE,sBA4G5B,CAAA"}
1
+ {"version":3,"file":"resolvePlugins.d.ts","sourceRoot":"","sources":["../../../src/resolve-modules/plugins/resolvePlugins.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAwBxD,eAAO,MAAM,cAAc,EAAE,sBAgH5B,CAAA"}
@@ -1,8 +1,11 @@
1
1
  import { Plugin } from "@inlang/plugin";
2
+ import { loadMessages as sdkLoadMessages, saveMessages as sdkSaveMessages, } from "../../persistence/plugin.js";
2
3
  import { PluginReturnedInvalidCustomApiError, PluginLoadMessagesFunctionAlreadyDefinedError, PluginSaveMessagesFunctionAlreadyDefinedError, PluginsDoNotProvideLoadOrSaveMessagesError, PluginHasInvalidIdError, PluginHasInvalidSchemaError, } from "./errors.js";
3
4
  import { deepmerge } from "deepmerge-ts";
4
5
  import { TypeCompiler } from "@sinclair/typebox/compiler";
5
6
  import { tryCatch } from "@inlang/result";
7
+ import _debug from "debug";
8
+ const debug = _debug("sdk:resolvePlugins");
6
9
  // @ts-ignore - type mismatch error
7
10
  const PluginCompiler = TypeCompiler.Compile(Plugin);
8
11
  export const resolvePlugins = async (args) => {
@@ -14,6 +17,10 @@ export const resolvePlugins = async (args) => {
14
17
  },
15
18
  errors: [],
16
19
  };
20
+ const experimentalPersistence = !!args.settings.experimental?.persistence;
21
+ if (experimentalPersistence) {
22
+ debug("Using experimental persistence");
23
+ }
17
24
  for (const plugin of args.plugins) {
18
25
  const errors = [...PluginCompiler.Errors(plugin)];
19
26
  /**
@@ -62,16 +69,10 @@ export const resolvePlugins = async (args) => {
62
69
  * -------------- BEGIN ADDING TO RESULT --------------
63
70
  */
64
71
  if (typeof plugin.loadMessages === "function") {
65
- result.data.loadMessages = (_args) => plugin.loadMessages({
66
- ..._args,
67
- // renoved nodeishFs from args because we need to pass custom wrapped fs that establishes a watcher
68
- });
72
+ result.data.loadMessages = plugin.loadMessages;
69
73
  }
70
74
  if (typeof plugin.saveMessages === "function") {
71
- result.data.saveMessages = (_args) => plugin.saveMessages({
72
- ..._args,
73
- nodeishFs: args.nodeishFs,
74
- });
75
+ result.data.saveMessages = plugin.saveMessages;
75
76
  }
76
77
  if (typeof plugin.addCustomApi === "function") {
77
78
  const { data: customApi } = tryCatch(() => plugin.addCustomApi({
@@ -83,7 +84,14 @@ export const resolvePlugins = async (args) => {
83
84
  }
84
85
  }
85
86
  // --- LOADMESSAGE / SAVEMESSAGE NOT DEFINED ---
86
- if (typeof result.data.loadMessages !== "function" ||
87
+ if (experimentalPersistence) {
88
+ debug("Override load/save for experimental persistence");
89
+ // @ts-ignore - type mismatch error
90
+ result.data.loadMessages = sdkLoadMessages;
91
+ // @ts-ignore - type mismatch error
92
+ result.data.saveMessages = sdkSaveMessages;
93
+ }
94
+ else if (typeof result.data.loadMessages !== "function" ||
87
95
  typeof result.data.saveMessages !== "function") {
88
96
  result.errors.push(new PluginsDoNotProvideLoadOrSaveMessagesError());
89
97
  }
@@ -49,7 +49,7 @@ it("should expose the project settings including the plugin settings", async ()
49
49
  nodeishFs: {},
50
50
  });
51
51
  await resolved.data.loadMessages({ settings, nodeishFs: {} });
52
- await resolved.data.saveMessages({ settings, messages: [] });
52
+ await resolved.data.saveMessages({ settings, messages: [], nodeishFs: {} });
53
53
  });
54
54
  describe("loadMessages", () => {
55
55
  it("should load messages from a local source", async () => {
@@ -31,6 +31,7 @@ export type ResolvedPluginApi = {
31
31
  saveMessages: (args: {
32
32
  settings: ProjectSettings;
33
33
  messages: Message[];
34
+ nodeishFs: NodeishFilesystemSubset;
34
35
  }) => Promise<void> | void;
35
36
  /**
36
37
  * App specific APIs.
@@ -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,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"}
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;QACpB,QAAQ,EAAE,eAAe,CAAA;QACzB,QAAQ,EAAE,OAAO,EAAE,CAAA;QACnB,SAAS,EAAE,uBAAuB,CAAA;KAClC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC1B;;;;;;;;;;;;;;;;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.34.1",
4
+ "version": "0.34.3",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -34,14 +34,14 @@
34
34
  "throttle-debounce": "^5.0.0",
35
35
  "@inlang/json-types": "1.1.0",
36
36
  "@inlang/language-tag": "1.5.1",
37
- "@inlang/message-lint-rule": "1.4.6",
38
- "@inlang/module": "1.2.10",
39
37
  "@inlang/message": "2.1.0",
40
- "@inlang/plugin": "2.4.10",
41
- "@inlang/project-settings": "2.4.1",
38
+ "@inlang/message-lint-rule": "1.4.7",
39
+ "@inlang/module": "1.2.11",
40
+ "@inlang/project-settings": "2.4.2",
42
41
  "@inlang/result": "1.1.0",
43
- "@lix-js/client": "1.4.0",
42
+ "@inlang/plugin": "2.4.11",
44
43
  "@inlang/translatable": "1.3.1",
44
+ "@lix-js/client": "1.4.0",
45
45
  "@lix-js/fs": "1.0.0"
46
46
  },
47
47
  "devDependencies": {
package/src/errors.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { SchemaOptions } from "@sinclair/typebox"
1
2
  import type { ValueError } from "@sinclair/typebox/errors"
2
3
 
3
4
  export class LoadProjectInvalidArgument extends Error {
@@ -11,15 +12,26 @@ export class ProjectSettingsInvalidError extends Error {
11
12
  constructor(options: { errors: ValueError[] }) {
12
13
  // TODO: beatufiy ValueErrors
13
14
  super(
14
- `The project settings are invalid:\n\n${options.errors
15
- .filter((error) => error.path)
16
- .map((error) => `"${error.path}":\n\n${error.message}`)
17
- .join("\n")}`
15
+ `The project settings are invalid:
16
+ ${options.errors
17
+ .filter((error) => error.path)
18
+ .map(FormatProjectSettingsError)
19
+ .join("\n")}`
18
20
  )
19
21
  this.name = "ProjectSettingsInvalidError"
20
22
  }
21
23
  }
22
24
 
25
+ function FormatProjectSettingsError(error: ValueError) {
26
+ let msg = `${error.message} at ${error.path}`
27
+ if (error.path.startsWith("/modules/")) {
28
+ msg += `
29
+ value = "${error.value}"
30
+ - ${error.schema.allOf.map((o: SchemaOptions) => `${o.description ?? ""}`).join("\n- ")}`
31
+ }
32
+ return msg
33
+ }
34
+
23
35
  export class ProjectSettingsFileJSONSyntaxError extends Error {
24
36
  constructor(options: { cause: ErrorOptions["cause"]; path: string }) {
25
37
  super(
@@ -38,6 +38,8 @@ import { capture } from "./telemetry/capture.js"
38
38
  import { identifyProject } from "./telemetry/groupIdentify.js"
39
39
  import type { NodeishStats } from "@lix-js/fs"
40
40
 
41
+ import type { ResolvedPluginApi } from "./resolve-modules/plugins/types.js"
42
+
41
43
  import _debug from "debug"
42
44
  const debug = _debug("sdk:loadProject")
43
45
  const debugLock = _debug("sdk:lockfile")
@@ -94,6 +96,7 @@ export async function loadProject(args: {
94
96
  // can't handle errors gracefully.
95
97
 
96
98
  assertValidProjectPath(projectPath)
99
+ debug(projectPath)
97
100
 
98
101
  const nodeishFs = createNodeishFsWithAbsolutePaths({
99
102
  projectPath,
@@ -142,6 +145,11 @@ export async function loadProject(args: {
142
145
  const setSettings = (settings: ProjectSettings): Result<void, ProjectSettingsInvalidError> => {
143
146
  try {
144
147
  const validatedSettings = parseSettings(settings)
148
+ if (validatedSettings.experimental?.persistence) {
149
+ settings["plugin.sdk.persistence"] = {
150
+ pathPattern: projectPath + "/messages.json",
151
+ }
152
+ }
145
153
  _setSettings(validatedSettings)
146
154
 
147
155
  writeSettingsToDisk(validatedSettings)
@@ -200,7 +208,9 @@ export async function loadProject(args: {
200
208
  const _resolvedModules = resolvedModules()
201
209
  if (!_resolvedModules) return
202
210
 
203
- if (!_resolvedModules.resolvedPluginApi.loadMessages) {
211
+ const resolvedPluginApi = _resolvedModules.resolvedPluginApi
212
+
213
+ if (!resolvedPluginApi.loadMessages) {
204
214
  markInitAsFailed(undefined)
205
215
  return
206
216
  }
@@ -208,11 +218,6 @@ export async function loadProject(args: {
208
218
  const _settings = settings()
209
219
  if (!_settings) return
210
220
 
211
- // get plugin finding the plugin that provides loadMessages function
212
- const loadMessagePlugin = _resolvedModules.plugins.find(
213
- (plugin) => plugin.loadMessages !== undefined
214
- )
215
-
216
221
  // TODO #1844 this watcher needs to get pruned when we have a change in the configs which will trigger this again
217
222
  const fsWithWatcher = createNodeishFsWithWatcher({
218
223
  nodeishFs: nodeishFs,
@@ -227,7 +232,7 @@ export async function loadProject(args: {
227
232
  messageStates,
228
233
  messagesQuery,
229
234
  settings()!, // NOTE we bang here - we don't expect the settings to become null during the livetime of a project
230
- loadMessagePlugin
235
+ resolvedPluginApi
231
236
  )
232
237
  .catch((e) => setLoadMessagesViaPluginError(new PluginLoadMessagesError({ cause: e })))
233
238
  .then(() => {
@@ -244,7 +249,7 @@ export async function loadProject(args: {
244
249
  messageStates,
245
250
  messagesQuery,
246
251
  _settings,
247
- loadMessagePlugin
252
+ resolvedPluginApi
248
253
  )
249
254
  .then(() => {
250
255
  markInitAsComplete()
@@ -303,19 +308,13 @@ export async function loadProject(args: {
303
308
 
304
309
  const _resolvedModules = resolvedModules()
305
310
  if (!_resolvedModules) return
311
+ const resolvedPluginApi = _resolvedModules.resolvedPluginApi
306
312
 
307
313
  const currentMessageIds = new Set(messagesQuery.includedMessageIds())
308
314
  const deletedTrackedMessages = [...trackedMessages].filter(
309
315
  (tracked) => !currentMessageIds.has(tracked[0])
310
316
  )
311
317
 
312
- const saveMessagesPlugin = _resolvedModules.plugins.find(
313
- (plugin) => plugin.saveMessages !== undefined
314
- )
315
- const loadMessagesPlugin = _resolvedModules.plugins.find(
316
- (plugin) => plugin.loadMessages !== undefined
317
- )
318
-
319
318
  for (const messageId of currentMessageIds) {
320
319
  if (!trackedMessages!.has(messageId!)) {
321
320
  // we create a new root to be able to cleanup an effect for a message that got deleted
@@ -334,6 +333,7 @@ export async function loadProject(args: {
334
333
 
335
334
  // don't trigger saves or set dirty flags during initial setup
336
335
  if (!initialSetup) {
336
+ debug("message changed", messageId)
337
337
  messageStates.messageDirtyFlags[message.id] = true
338
338
  saveMessagesViaPlugin(
339
339
  nodeishFs,
@@ -341,8 +341,7 @@ export async function loadProject(args: {
341
341
  messageStates,
342
342
  messagesQuery,
343
343
  settings()!,
344
- saveMessagesPlugin,
345
- loadMessagesPlugin
344
+ resolvedPluginApi
346
345
  )
347
346
  .catch((e) =>
348
347
  setSaveMessagesViaPluginError(new PluginSaveMessagesError({ cause: e }))
@@ -379,8 +378,7 @@ export async function loadProject(args: {
379
378
  messageStates,
380
379
  messagesQuery,
381
380
  settings()!,
382
- saveMessagesPlugin,
383
- loadMessagesPlugin
381
+ resolvedPluginApi
384
382
  )
385
383
  .catch((e) => setSaveMessagesViaPluginError(new PluginSaveMessagesError({ cause: e })))
386
384
  .then(() => {
@@ -608,7 +606,7 @@ async function loadMessagesViaPlugin(
608
606
  messageState: MessageState,
609
607
  messagesQuery: InlangProject["query"]["messages"],
610
608
  settingsValue: ProjectSettings,
611
- loadPlugin: any
609
+ resolvedPluginApi: ResolvedPluginApi
612
610
  ) {
613
611
  const experimentalAliases = !!settingsValue.experimental?.aliases
614
612
 
@@ -628,7 +626,7 @@ async function loadMessagesViaPlugin(
628
626
  try {
629
627
  lockTime = await acquireFileLock(fs as NodeishFilesystem, lockDirPath, "loadMessage")
630
628
  const loadedMessages = await makeTrulyAsync(
631
- loadPlugin.loadMessages({
629
+ resolvedPluginApi.loadMessages({
632
630
  settings: settingsValue,
633
631
  nodeishFs: fs,
634
632
  })
@@ -728,7 +726,14 @@ async function loadMessagesViaPlugin(
728
726
  messageState.sheduledLoadMessagesViaPlugin = undefined
729
727
 
730
728
  // recall load unawaited to allow stack to pop
731
- loadMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuery, settingsValue, loadPlugin)
729
+ loadMessagesViaPlugin(
730
+ fs,
731
+ lockDirPath,
732
+ messageState,
733
+ messagesQuery,
734
+ settingsValue,
735
+ resolvedPluginApi
736
+ )
732
737
  .then(() => {
733
738
  // resolve the scheduled load message promise
734
739
  executingScheduledMessages[1]()
@@ -746,8 +751,7 @@ async function saveMessagesViaPlugin(
746
751
  messageState: MessageState,
747
752
  messagesQuery: InlangProject["query"]["messages"],
748
753
  settingsValue: ProjectSettings,
749
- savePlugin: any,
750
- loadPlugin: any
754
+ resolvedPluginApi: ResolvedPluginApi
751
755
  ): Promise<void> {
752
756
  // queue next save if we have a save ongoing
753
757
  if (messageState.isSaving) {
@@ -809,7 +813,7 @@ async function saveMessagesViaPlugin(
809
813
  messageState.messageDirtyFlags = {}
810
814
 
811
815
  // NOTE: this assumes that the plugin will handle message ordering
812
- await savePlugin.saveMessages({
816
+ await resolvedPluginApi.saveMessages({
813
817
  settings: settingsValue,
814
818
  messages: messagesToExport,
815
819
  nodeishFs: fs,
@@ -833,7 +837,7 @@ async function saveMessagesViaPlugin(
833
837
  messageState,
834
838
  messagesQuery,
835
839
  settingsValue,
836
- loadPlugin
840
+ resolvedPluginApi
837
841
  )
838
842
  }
839
843
 
@@ -877,8 +881,7 @@ async function saveMessagesViaPlugin(
877
881
  messageState,
878
882
  messagesQuery,
879
883
  settingsValue,
880
- savePlugin,
881
- loadPlugin
884
+ resolvedPluginApi
882
885
  )
883
886
  .then(() => {
884
887
  executingSheduledSaveMessages[1]()
@@ -0,0 +1,60 @@
1
+ import { test, expect } from "vitest"
2
+ import { createMessage, createNodeishMemoryFs } from "../test-utilities/index.js"
3
+ import { normalizeMessage } from "../storage/helper.js"
4
+ import { pluginId } from "./plugin.js"
5
+
6
+ const mockMessages = [
7
+ createMessage("first_message", {
8
+ en: "If this fails I will be sad",
9
+ }),
10
+ createMessage("second_message", {
11
+ en: "Let's see if this works",
12
+ de: "Mal sehen ob das funktioniert",
13
+ }),
14
+ ]
15
+
16
+ // the test ensures:
17
+ // - messages can be loaded
18
+ // - messages can be saved
19
+ // - after loading and saving messages, the state is the same as before (roundtrip)
20
+ test("roundtrip (saving/loading messages)", async () => {
21
+ const { loadMessages, saveMessages } = await import("./plugin.js")
22
+ const fs = createNodeishMemoryFs()
23
+ const projectDir = "/test/project.inlang"
24
+ const pathPattern = projectDir + "/messages.json"
25
+ const persistedMessages = JSON.stringify(mockMessages.map(normalizeMessage), undefined, 2)
26
+
27
+ const settings = {
28
+ sourceLanguageTag: "en",
29
+ languageTags: ["en", "de"],
30
+ modules: [],
31
+ [pluginId]: { pathPattern },
32
+ }
33
+
34
+ await fs.mkdir(projectDir, { recursive: true })
35
+ await fs.writeFile(pathPattern, persistedMessages)
36
+
37
+ const firstMessageLoad = await loadMessages({
38
+ settings,
39
+ nodeishFs: fs,
40
+ })
41
+
42
+ expect(firstMessageLoad).toStrictEqual(mockMessages)
43
+
44
+ await saveMessages({
45
+ settings,
46
+ nodeishFs: fs,
47
+ messages: firstMessageLoad,
48
+ })
49
+
50
+ const afterRoundtrip = await fs.readFile(pathPattern, { encoding: "utf-8" })
51
+
52
+ expect(afterRoundtrip).toStrictEqual(persistedMessages)
53
+
54
+ const messagesAfterRoundtrip = await loadMessages({
55
+ settings,
56
+ nodeishFs: fs,
57
+ })
58
+
59
+ expect(messagesAfterRoundtrip).toStrictEqual(firstMessageLoad)
60
+ })
@@ -0,0 +1,56 @@
1
+ import type { ProjectSettings, Message } from "@inlang/sdk"
2
+ import { getDirname, type NodeishFilesystem } from "@lix-js/fs"
3
+ import { normalizeMessage } from "../storage/helper.js"
4
+
5
+ import _debug from "debug"
6
+ const debug = _debug("sdk:persistence")
7
+
8
+ export const pluginId = "plugin.sdk.persistence"
9
+
10
+ export async function loadMessages(args: {
11
+ settings: ProjectSettings
12
+ nodeishFs: NodeishFilesystem
13
+ }) {
14
+ let result: Message[] = []
15
+ const pathPattern = args.settings[pluginId]?.pathPattern as string
16
+
17
+ debug("loadMessages", pathPattern)
18
+ try {
19
+ const file = await args.nodeishFs.readFile(pathPattern, { encoding: "utf-8" })
20
+ result = JSON.parse(file)
21
+ } catch (error) {
22
+ if ((error as any)?.code !== "ENOENT") {
23
+ debug("loadMessages", error)
24
+ throw error
25
+ }
26
+ }
27
+ return result
28
+ }
29
+
30
+ export async function saveMessages(args: {
31
+ settings: ProjectSettings
32
+ nodeishFs: NodeishFilesystem
33
+ messages: Message[]
34
+ }) {
35
+ const pathPattern = args.settings[pluginId]?.pathPattern as string
36
+
37
+ debug("saveMessages", pathPattern)
38
+ try {
39
+ await createDirectoryIfNotExits(getDirname(pathPattern), args.nodeishFs)
40
+ await args.nodeishFs.writeFile(
41
+ pathPattern,
42
+ // 2 spaces indentation
43
+ JSON.stringify(args.messages.map(normalizeMessage), undefined, 2)
44
+ )
45
+ } catch (error) {
46
+ debug("saveMessages", error)
47
+ }
48
+ }
49
+
50
+ async function createDirectoryIfNotExits(path: string, nodeishFs: NodeishFilesystem) {
51
+ try {
52
+ await nodeishFs.mkdir(path, { recursive: true })
53
+ } catch {
54
+ // assume that the directory already exists
55
+ }
56
+ }
@@ -61,7 +61,7 @@ it("should expose the project settings including the plugin settings", async ()
61
61
  nodeishFs: {} as any,
62
62
  })
63
63
  await resolved.data.loadMessages!({ settings, nodeishFs: {} as any })
64
- await resolved.data.saveMessages!({ settings, messages: [] })
64
+ await resolved.data.saveMessages!({ settings, messages: [], nodeishFs: {} as any })
65
65
  })
66
66
 
67
67
  describe("loadMessages", () => {
@@ -1,6 +1,10 @@
1
1
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
2
  import type { ResolvePluginsFunction } from "./types.js"
3
3
  import { Plugin } from "@inlang/plugin"
4
+ import {
5
+ loadMessages as sdkLoadMessages,
6
+ saveMessages as sdkSaveMessages,
7
+ } from "../../persistence/plugin.js"
4
8
  import {
5
9
  PluginReturnedInvalidCustomApiError,
6
10
  PluginLoadMessagesFunctionAlreadyDefinedError,
@@ -13,6 +17,9 @@ import { deepmerge } from "deepmerge-ts"
13
17
  import { TypeCompiler } from "@sinclair/typebox/compiler"
14
18
  import { tryCatch } from "@inlang/result"
15
19
 
20
+ import _debug from "debug"
21
+ const debug = _debug("sdk:resolvePlugins")
22
+
16
23
  // @ts-ignore - type mismatch error
17
24
  const PluginCompiler = TypeCompiler.Compile(Plugin)
18
25
 
@@ -26,6 +33,11 @@ export const resolvePlugins: ResolvePluginsFunction = async (args) => {
26
33
  errors: [],
27
34
  }
28
35
 
36
+ const experimentalPersistence = !!args.settings.experimental?.persistence
37
+ if (experimentalPersistence) {
38
+ debug("Using experimental persistence")
39
+ }
40
+
29
41
  for (const plugin of args.plugins) {
30
42
  const errors = [...PluginCompiler.Errors(plugin)]
31
43
 
@@ -88,19 +100,11 @@ export const resolvePlugins: ResolvePluginsFunction = async (args) => {
88
100
  */
89
101
 
90
102
  if (typeof plugin.loadMessages === "function") {
91
- result.data.loadMessages = (_args) =>
92
- plugin.loadMessages!({
93
- ..._args,
94
- // renoved nodeishFs from args because we need to pass custom wrapped fs that establishes a watcher
95
- })
103
+ result.data.loadMessages = plugin.loadMessages
96
104
  }
97
105
 
98
106
  if (typeof plugin.saveMessages === "function") {
99
- result.data.saveMessages = (_args) =>
100
- plugin.saveMessages!({
101
- ..._args,
102
- nodeishFs: args.nodeishFs,
103
- })
107
+ result.data.saveMessages = plugin.saveMessages
104
108
  }
105
109
 
106
110
  if (typeof plugin.addCustomApi === "function") {
@@ -116,7 +120,14 @@ export const resolvePlugins: ResolvePluginsFunction = async (args) => {
116
120
  }
117
121
 
118
122
  // --- LOADMESSAGE / SAVEMESSAGE NOT DEFINED ---
119
- if (
123
+
124
+ if (experimentalPersistence) {
125
+ debug("Override load/save for experimental persistence")
126
+ // @ts-ignore - type mismatch error
127
+ result.data.loadMessages = sdkLoadMessages
128
+ // @ts-ignore - type mismatch error
129
+ result.data.saveMessages = sdkSaveMessages
130
+ } else if (
120
131
  typeof result.data.loadMessages !== "function" ||
121
132
  typeof result.data.saveMessages !== "function"
122
133
  ) {
@@ -48,7 +48,11 @@ export type ResolvedPluginApi = {
48
48
  settings: ProjectSettings
49
49
  nodeishFs: NodeishFilesystemSubset
50
50
  }) => Promise<Message[]> | Message[]
51
- saveMessages: (args: { settings: ProjectSettings; messages: Message[] }) => Promise<void> | void
51
+ saveMessages: (args: {
52
+ settings: ProjectSettings
53
+ messages: Message[]
54
+ nodeishFs: NodeishFilesystemSubset
55
+ }) => Promise<void> | void
52
56
  /**
53
57
  * App specific APIs.
54
58
  *