@inlang/sdk 0.33.0 → 0.34.1

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/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export { listProjects } from "./listProjects.js";
12
12
  export { solidAdapter, type InlangProjectWithSolidAdapter } from "./adapter/solidAdapter.js";
13
13
  export { createMessagesQuery } from "./createMessagesQuery.js";
14
14
  export { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginLoadMessagesError, PluginSaveMessagesError, } from "./errors.js";
15
+ export { normalizeMessage } from "./storage/helper.js";
15
16
  export * from "./messages/variant.js";
16
17
  export * from "./versionedInterfaces.js";
17
18
  export { InlangModule } from "@inlang/module";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EACX,aAAa,EACb,wBAAwB,EACxB,eAAe,EACf,eAAe,EACf,YAAY,GACZ,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,KAAK,6BAA6B,EAAE,MAAM,2BAA2B,CAAA;AAC5F,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EACN,kCAAkC,EAClC,gCAAgC,EAChC,2BAA2B,EAC3B,uBAAuB,EACvB,uBAAuB,GACvB,MAAM,aAAa,CAAA;AAEpB,cAAc,uBAAuB,CAAA;AACrC,cAAc,0BAA0B,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EACX,aAAa,EACb,wBAAwB,EACxB,eAAe,EACf,eAAe,EACf,YAAY,GACZ,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,KAAK,6BAA6B,EAAE,MAAM,2BAA2B,CAAA;AAC5F,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EACN,kCAAkC,EAClC,gCAAgC,EAChC,2BAA2B,EAC3B,uBAAuB,EACvB,uBAAuB,GACvB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AACtD,cAAc,uBAAuB,CAAA;AACrC,cAAc,0BAA0B,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ export { listProjects } from "./listProjects.js";
11
11
  export { solidAdapter } from "./adapter/solidAdapter.js";
12
12
  export { createMessagesQuery } from "./createMessagesQuery.js";
13
13
  export { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginLoadMessagesError, PluginSaveMessagesError, } from "./errors.js";
14
+ export { normalizeMessage } from "./storage/helper.js";
14
15
  export * from "./messages/variant.js";
15
16
  export * from "./versionedInterfaces.js";
16
17
  export { InlangModule } from "@inlang/module";
@@ -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;AAgChD;;;;;;;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;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"}
@@ -18,7 +18,8 @@ import { maybeCreateFirstProjectId } from "./migrations/maybeCreateFirstProjectI
18
18
  import { capture } from "./telemetry/capture.js";
19
19
  import { identifyProject } from "./telemetry/groupIdentify.js";
20
20
  import _debug from "debug";
21
- const debug = _debug("loadProject");
21
+ const debug = _debug("sdk:loadProject");
22
+ const debugLock = _debug("sdk:lockfile");
22
23
  const settingsCompiler = TypeCompiler.Compile(ProjectSettings);
23
24
  /**
24
25
  * @param projectPath - Absolute path to the inlang settings file.
@@ -449,7 +450,7 @@ async function loadMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuer
449
450
  const importedEnecoded = stringifyMessage(loadedMessageClone);
450
451
  // NOTE could use hash instead of the whole object JSON to save memory...
451
452
  if (messageState.messageLoadHash[loadedMessageClone.id] === importedEnecoded) {
452
- debug("skipping upsert!");
453
+ // debug("skipping upsert!")
453
454
  continue;
454
455
  }
455
456
  // This logic is preventing cycles - could also be handled if update api had a parameter for who triggered update
@@ -630,10 +631,10 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
630
631
  throw new Error(lockOrigin + " exceeded maximum Retries (5) to acquire lockfile " + tryCount);
631
632
  }
632
633
  try {
633
- debug(lockOrigin + " tries to acquire a lockfile Retry Nr.: " + tryCount);
634
+ debugLock(lockOrigin + " tries to acquire a lockfile Retry Nr.: " + tryCount);
634
635
  await fs.mkdir(lockDirPath);
635
636
  const stats = await fs.stat(lockDirPath);
636
- debug(lockOrigin + " acquired a lockfile Retry Nr.: " + tryCount);
637
+ debugLock(lockOrigin + " acquired a lockfile Retry Nr.: " + tryCount);
637
638
  return stats.mtimeMs;
638
639
  }
639
640
  catch (error) {
@@ -650,12 +651,12 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
650
651
  catch (fstatError) {
651
652
  if (fstatError.code === "ENOENT") {
652
653
  // lock file seems to be gone :) - lets try again
653
- debug(lockOrigin + " tryCount++ lock file seems to be gone :) - lets try again " + tryCount);
654
+ debugLock(lockOrigin + " tryCount++ lock file seems to be gone :) - lets try again " + tryCount);
654
655
  return acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1);
655
656
  }
656
657
  throw fstatError;
657
658
  }
658
- debug(lockOrigin +
659
+ debugLock(lockOrigin +
659
660
  " tries to acquire a lockfile - lock currently in use... starting probe phase " +
660
661
  tryCount);
661
662
  return new Promise((resolve, reject) => {
@@ -665,13 +666,13 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
665
666
  probeCounts += 1;
666
667
  let lockFileStats = undefined;
667
668
  try {
668
- debug(lockOrigin + " tries to acquire a lockfile - check if the lock is free now " + tryCount);
669
+ debugLock(lockOrigin + " tries to acquire a lockfile - check if the lock is free now " + tryCount);
669
670
  // alright lets give it another try
670
671
  lockFileStats = await fs.stat(lockDirPath);
671
672
  }
672
673
  catch (fstatError) {
673
674
  if (fstatError.code === "ENOENT") {
674
- debug(lockOrigin +
675
+ debugLock(lockOrigin +
675
676
  " tryCount++ in Promise - tries to acquire a lockfile - lock file seems to be free now - try to acquire " +
676
677
  tryCount);
677
678
  const lock = acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1);
@@ -683,7 +684,7 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
683
684
  if (lockFileStats.mtimeMs === currentLockTime) {
684
685
  if (probeCounts >= nProbes) {
685
686
  // ok maximum lock time ran up (we waitetd nProbes * probeInterval) - we consider the lock to be stale
686
- debug(lockOrigin +
687
+ debugLock(lockOrigin +
687
688
  " tries to acquire a lockfile - lock not free - but stale lets drop it" +
688
689
  tryCount);
689
690
  try {
@@ -698,7 +699,7 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
698
699
  return reject(rmLockError);
699
700
  }
700
701
  try {
701
- debug(lockOrigin +
702
+ debugLock(lockOrigin +
702
703
  " tryCount++ same locker - try to acquire again after removing stale lock " +
703
704
  tryCount);
704
705
  const lock = await acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1);
@@ -715,7 +716,7 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
715
716
  }
716
717
  else {
717
718
  try {
718
- debug(lockOrigin + " tryCount++ different locker - try to acquire again " + tryCount);
719
+ debugLock(lockOrigin + " tryCount++ different locker - try to acquire again " + tryCount);
719
720
  const lock = await acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1);
720
721
  return resolve(lock);
721
722
  }
@@ -729,7 +730,7 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
729
730
  });
730
731
  }
731
732
  async function releaseLock(fs, lockDirPath, lockOrigin, lockTime) {
732
- debug(lockOrigin + " releasing the lock ");
733
+ debugLock(lockOrigin + " releasing the lock ");
733
734
  try {
734
735
  const stats = await fs.stat(lockDirPath);
735
736
  if (stats.mtimeMs === lockTime) {
@@ -738,13 +739,13 @@ async function releaseLock(fs, lockDirPath, lockOrigin, lockTime) {
738
739
  }
739
740
  }
740
741
  catch (statError) {
741
- debug(lockOrigin + " couldn't release the lock");
742
+ debugLock(lockOrigin + " couldn't release the lock");
742
743
  if (statError.code === "ENOENT") {
743
744
  // ok seeks like the log was released by someone else
744
- debug(lockOrigin + " WARNING - the lock was released by a different process");
745
+ debugLock(lockOrigin + " WARNING - the lock was released by a different process");
745
746
  return;
746
747
  }
747
- debug(statError);
748
+ debugLock(statError);
748
749
  throw statError;
749
750
  }
750
751
  }
@@ -1,5 +1,29 @@
1
1
  import { Message } from "../versionedInterfaces.js";
2
2
  export declare function getMessageIdFromPath(path: string): string | undefined;
3
3
  export declare function getPathFromMessageId(id: string): string;
4
+ /**
5
+ * Returns a copy of a message object with sorted variants and object keys.
6
+ * This produces a deterministic result when passed to stringify
7
+ * independent of the initialization order.
8
+ */
9
+ export declare function normalizeMessage(message: Message): {
10
+ id: string;
11
+ alias: Record<string, string>;
12
+ selectors: {
13
+ type: "VariableReference";
14
+ name: string;
15
+ }[];
16
+ variants: {
17
+ languageTag: string;
18
+ match: string[];
19
+ pattern: ({
20
+ type: "Text";
21
+ value: string;
22
+ } | {
23
+ type: "VariableReference";
24
+ name: string;
25
+ })[];
26
+ }[];
27
+ };
4
28
  export declare function stringifyMessage(message: Message): string;
5
29
  //# sourceMappingURL=helper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../../src/storage/helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAW,MAAM,2BAA2B,CAAA;AAI5D,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,sBAahD;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,UAG9C;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,UAuBhD"}
1
+ {"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../../src/storage/helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAW,MAAM,2BAA2B,CAAA;AAI5D,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,sBAahD;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,UAG9C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO;;;;;;;;;;;;;;;;;;EAwChD;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,UAEhD"}
@@ -15,21 +15,49 @@ export function getPathFromMessageId(id) {
15
15
  const path = id.replace("_", "/") + fileExtension;
16
16
  return path;
17
17
  }
18
- export function stringifyMessage(message) {
19
- // create a new object do specify key output order
18
+ /**
19
+ * Returns a copy of a message object with sorted variants and object keys.
20
+ * This produces a deterministic result when passed to stringify
21
+ * independent of the initialization order.
22
+ */
23
+ export function normalizeMessage(message) {
24
+ // order keys in message
20
25
  const messageWithSortedKeys = {};
21
26
  for (const key of Object.keys(message).sort()) {
22
27
  messageWithSortedKeys[key] = message[key];
23
28
  }
24
- // lets order variants as well
25
- messageWithSortedKeys["variants"] = messageWithSortedKeys["variants"].sort((variantA, variantB) => {
26
- // First, compare by language
29
+ // order variants
30
+ messageWithSortedKeys["variants"] = messageWithSortedKeys["variants"]
31
+ .sort((variantA, variantB) => {
32
+ // compare by language
27
33
  const languageComparison = variantA.languageTag.localeCompare(variantB.languageTag);
28
- // If languages are the same, compare by match
34
+ // if languages are the same, compare by match
29
35
  if (languageComparison === 0) {
30
36
  return variantA.match.join("-").localeCompare(variantB.match.join("-"));
31
37
  }
32
38
  return languageComparison;
39
+ })
40
+ // order keys in each variant
41
+ .map((variant) => {
42
+ const variantWithSortedKeys = {};
43
+ for (const variantKey of Object.keys(variant).sort()) {
44
+ if (variantKey === "pattern") {
45
+ variantWithSortedKeys[variantKey] = variant["pattern"].map((token) => {
46
+ const tokenWithSortedKey = {};
47
+ for (const tokenKey of Object.keys(token).sort()) {
48
+ tokenWithSortedKey[tokenKey] = token[tokenKey];
49
+ }
50
+ return tokenWithSortedKey;
51
+ });
52
+ }
53
+ else {
54
+ variantWithSortedKeys[variantKey] = variant[variantKey];
55
+ }
56
+ }
57
+ return variantWithSortedKeys;
33
58
  });
34
- return JSON.stringify(messageWithSortedKeys, undefined, 4);
59
+ return messageWithSortedKeys;
60
+ }
61
+ export function stringifyMessage(message) {
62
+ return JSON.stringify(normalizeMessage(message), undefined, 4);
35
63
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=helpers.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.test.d.ts","sourceRoot":"","sources":["../../src/storage/helpers.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect } from "vitest";
2
+ // import { parseLixUri, parseOrigin } from "./helpers.js"
3
+ import { normalizeMessage, stringifyMessage } from "./helper.js";
4
+ const unsortedMessageRaw = {
5
+ alias: {},
6
+ selectors: [],
7
+ id: "footer_categories_apps",
8
+ variants: [
9
+ { languageTag: "a", match: ["*", "1"], pattern: [{ type: "Text", value: "2" }] },
10
+ { languageTag: "a", match: ["*", "*"], pattern: [{ type: "Text", value: "1" }] },
11
+ {
12
+ languageTag: "a",
13
+ match: ["1", "*"],
14
+ pattern: [
15
+ { type: "Text", value: "2" },
16
+ { type: "Text", value: "2" },
17
+ ],
18
+ },
19
+ { languageTag: "b", match: [], pattern: [{ type: "Text", value: "4" }] },
20
+ { languageTag: "a", match: ["1", "1"], pattern: [{ type: "Text", value: "2" }] },
21
+ { languageTag: "c", match: [], pattern: [{ value: "5", type: "Text" }] },
22
+ { match: [], languageTag: "d", pattern: [{ type: "Text", value: "6" }] },
23
+ { languageTag: "e", match: [], pattern: [{ type: "Text", value: "7" }] },
24
+ { languageTag: "f", match: [], pattern: [{ type: "Text", value: "8" }] },
25
+ { languageTag: "g", match: [], pattern: [{ type: "Text", value: "9" }] },
26
+ ],
27
+ };
28
+ const sortedMessageRaw = {
29
+ alias: {},
30
+ id: "footer_categories_apps",
31
+ selectors: [],
32
+ variants: [
33
+ { languageTag: "a", match: ["*", "*"], pattern: [{ type: "Text", value: "1" }] },
34
+ { languageTag: "a", match: ["*", "1"], pattern: [{ type: "Text", value: "2" }] },
35
+ {
36
+ languageTag: "a",
37
+ match: ["1", "*"],
38
+ pattern: [
39
+ { type: "Text", value: "2" },
40
+ { type: "Text", value: "2" },
41
+ ],
42
+ },
43
+ { languageTag: "a", match: ["1", "1"], pattern: [{ type: "Text", value: "2" }] },
44
+ { languageTag: "b", match: [], pattern: [{ type: "Text", value: "4" }] },
45
+ { languageTag: "c", match: [], pattern: [{ type: "Text", value: "5" }] },
46
+ { languageTag: "d", match: [], pattern: [{ type: "Text", value: "6" }] },
47
+ { languageTag: "e", match: [], pattern: [{ type: "Text", value: "7" }] },
48
+ { languageTag: "f", match: [], pattern: [{ type: "Text", value: "8" }] },
49
+ { languageTag: "g", match: [], pattern: [{ type: "Text", value: "9" }] },
50
+ ],
51
+ };
52
+ // stringify with no indentation
53
+ function str(obj) {
54
+ return JSON.stringify(obj);
55
+ }
56
+ // stringify with 2 space indentation
57
+ function str2(obj) {
58
+ return JSON.stringify(obj, undefined, 2);
59
+ }
60
+ // stringify with 4 space indentation
61
+ function str4(obj) {
62
+ return JSON.stringify(obj, undefined, 4);
63
+ }
64
+ describe("normalizeMessage", () => {
65
+ it("should return the message with sorted keys and variants", () => {
66
+ // test cases are not the same (deep equal) before normalization
67
+ // array order of variants is different
68
+ expect(unsortedMessageRaw).not.toEqual(sortedMessageRaw);
69
+ // test cases are the same after normalization
70
+ expect(normalizeMessage(unsortedMessageRaw)).toEqual(sortedMessageRaw);
71
+ // stringify results are not the same before normalization
72
+ expect(str(unsortedMessageRaw)).not.toBe(str(sortedMessageRaw));
73
+ // stringify results are the same after normalization
74
+ expect(str(normalizeMessage(unsortedMessageRaw))).toBe(str(sortedMessageRaw));
75
+ expect(str2(normalizeMessage(unsortedMessageRaw))).toBe(str2(sortedMessageRaw));
76
+ expect(str4(normalizeMessage(unsortedMessageRaw))).toBe(str4(sortedMessageRaw));
77
+ });
78
+ });
79
+ describe("stringifyMessage", () => {
80
+ it("should normalize and JSON stringify a message with 4 space indentation", () => {
81
+ expect(stringifyMessage(unsortedMessageRaw)).toBe(str4(sortedMessageRaw));
82
+ expect(stringifyMessage(sortedMessageRaw)).toBe(str4(sortedMessageRaw));
83
+ });
84
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inlang/sdk",
3
3
  "type": "module",
4
- "version": "0.33.0",
4
+ "version": "0.34.1",
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",
37
39
  "@inlang/message": "2.1.0",
38
- "@inlang/message-lint-rule": "1.4.5",
39
- "@inlang/module": "1.2.9",
40
- "@inlang/plugin": "2.4.9",
40
+ "@inlang/plugin": "2.4.10",
41
+ "@inlang/project-settings": "2.4.1",
41
42
  "@inlang/result": "1.1.0",
42
- "@inlang/project-settings": "2.4.0",
43
+ "@lix-js/client": "1.4.0",
43
44
  "@inlang/translatable": "1.3.1",
44
- "@lix-js/client": "1.2.1",
45
45
  "@lix-js/fs": "1.0.0"
46
46
  },
47
47
  "devDependencies": {
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export {
26
26
  PluginSaveMessagesError,
27
27
  } from "./errors.js"
28
28
 
29
+ export { normalizeMessage } from "./storage/helper.js"
29
30
  export * from "./messages/variant.js"
30
31
  export * from "./versionedInterfaces.js"
31
32
  export { InlangModule } from "@inlang/module"
@@ -39,7 +39,8 @@ import { identifyProject } from "./telemetry/groupIdentify.js"
39
39
  import type { NodeishStats } from "@lix-js/fs"
40
40
 
41
41
  import _debug from "debug"
42
- const debug = _debug("loadProject")
42
+ const debug = _debug("sdk:loadProject")
43
+ const debugLock = _debug("sdk:lockfile")
43
44
 
44
45
  const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
45
46
 
@@ -663,7 +664,7 @@ async function loadMessagesViaPlugin(
663
664
 
664
665
  // NOTE could use hash instead of the whole object JSON to save memory...
665
666
  if (messageState.messageLoadHash[loadedMessageClone.id] === importedEnecoded) {
666
- debug("skipping upsert!")
667
+ // debug("skipping upsert!")
667
668
  continue
668
669
  }
669
670
 
@@ -902,10 +903,10 @@ async function acquireFileLock(
902
903
  }
903
904
 
904
905
  try {
905
- debug(lockOrigin + " tries to acquire a lockfile Retry Nr.: " + tryCount)
906
+ debugLock(lockOrigin + " tries to acquire a lockfile Retry Nr.: " + tryCount)
906
907
  await fs.mkdir(lockDirPath)
907
908
  const stats = await fs.stat(lockDirPath)
908
- debug(lockOrigin + " acquired a lockfile Retry Nr.: " + tryCount)
909
+ debugLock(lockOrigin + " acquired a lockfile Retry Nr.: " + tryCount)
909
910
  return stats.mtimeMs
910
911
  } catch (error: any) {
911
912
  if (error.code !== "EEXIST") {
@@ -922,12 +923,14 @@ async function acquireFileLock(
922
923
  } catch (fstatError: any) {
923
924
  if (fstatError.code === "ENOENT") {
924
925
  // lock file seems to be gone :) - lets try again
925
- debug(lockOrigin + " tryCount++ lock file seems to be gone :) - lets try again " + tryCount)
926
+ debugLock(
927
+ lockOrigin + " tryCount++ lock file seems to be gone :) - lets try again " + tryCount
928
+ )
926
929
  return acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1)
927
930
  }
928
931
  throw fstatError
929
932
  }
930
- debug(
933
+ debugLock(
931
934
  lockOrigin +
932
935
  " tries to acquire a lockfile - lock currently in use... starting probe phase " +
933
936
  tryCount
@@ -940,7 +943,7 @@ async function acquireFileLock(
940
943
  probeCounts += 1
941
944
  let lockFileStats: undefined | NodeishStats = undefined
942
945
  try {
943
- debug(
946
+ debugLock(
944
947
  lockOrigin + " tries to acquire a lockfile - check if the lock is free now " + tryCount
945
948
  )
946
949
 
@@ -948,7 +951,7 @@ async function acquireFileLock(
948
951
  lockFileStats = await fs.stat(lockDirPath)
949
952
  } catch (fstatError: any) {
950
953
  if (fstatError.code === "ENOENT") {
951
- debug(
954
+ debugLock(
952
955
  lockOrigin +
953
956
  " tryCount++ in Promise - tries to acquire a lockfile - lock file seems to be free now - try to acquire " +
954
957
  tryCount
@@ -963,7 +966,7 @@ async function acquireFileLock(
963
966
  if (lockFileStats.mtimeMs === currentLockTime) {
964
967
  if (probeCounts >= nProbes) {
965
968
  // ok maximum lock time ran up (we waitetd nProbes * probeInterval) - we consider the lock to be stale
966
- debug(
969
+ debugLock(
967
970
  lockOrigin +
968
971
  " tries to acquire a lockfile - lock not free - but stale lets drop it" +
969
972
  tryCount
@@ -979,7 +982,7 @@ async function acquireFileLock(
979
982
  return reject(rmLockError)
980
983
  }
981
984
  try {
982
- debug(
985
+ debugLock(
983
986
  lockOrigin +
984
987
  " tryCount++ same locker - try to acquire again after removing stale lock " +
985
988
  tryCount
@@ -995,7 +998,9 @@ async function acquireFileLock(
995
998
  }
996
999
  } else {
997
1000
  try {
998
- debug(lockOrigin + " tryCount++ different locker - try to acquire again " + tryCount)
1001
+ debugLock(
1002
+ lockOrigin + " tryCount++ different locker - try to acquire again " + tryCount
1003
+ )
999
1004
  const lock = await acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1)
1000
1005
  return resolve(lock)
1001
1006
  } catch (error) {
@@ -1014,7 +1019,7 @@ async function releaseLock(
1014
1019
  lockOrigin: string,
1015
1020
  lockTime: number
1016
1021
  ) {
1017
- debug(lockOrigin + " releasing the lock ")
1022
+ debugLock(lockOrigin + " releasing the lock ")
1018
1023
  try {
1019
1024
  const stats = await fs.stat(lockDirPath)
1020
1025
  if (stats.mtimeMs === lockTime) {
@@ -1022,13 +1027,13 @@ async function releaseLock(
1022
1027
  await fs.rmdir(lockDirPath)
1023
1028
  }
1024
1029
  } catch (statError: any) {
1025
- debug(lockOrigin + " couldn't release the lock")
1030
+ debugLock(lockOrigin + " couldn't release the lock")
1026
1031
  if (statError.code === "ENOENT") {
1027
1032
  // ok seeks like the log was released by someone else
1028
- debug(lockOrigin + " WARNING - the lock was released by a different process")
1033
+ debugLock(lockOrigin + " WARNING - the lock was released by a different process")
1029
1034
  return
1030
1035
  }
1031
- debug(statError)
1036
+ debugLock(statError)
1032
1037
  throw statError
1033
1038
  }
1034
1039
  }
@@ -28,4 +28,4 @@ it("should return undefined if repoMeta contains error", async () => {
28
28
  projectPath: "mocked_project_path",
29
29
  })
30
30
  expect(projectId).toBeUndefined()
31
- })
31
+ })
@@ -22,27 +22,53 @@ export function getPathFromMessageId(id: string) {
22
22
  return path
23
23
  }
24
24
 
25
- export function stringifyMessage(message: Message) {
26
- // create a new object do specify key output order
25
+ /**
26
+ * Returns a copy of a message object with sorted variants and object keys.
27
+ * This produces a deterministic result when passed to stringify
28
+ * independent of the initialization order.
29
+ */
30
+ export function normalizeMessage(message: Message) {
31
+ // order keys in message
27
32
  const messageWithSortedKeys: any = {}
28
33
  for (const key of Object.keys(message).sort()) {
29
34
  messageWithSortedKeys[key] = (message as any)[key]
30
35
  }
31
36
 
32
- // lets order variants as well
33
- messageWithSortedKeys["variants"] = messageWithSortedKeys["variants"].sort(
34
- (variantA: Variant, variantB: Variant) => {
35
- // First, compare by language
37
+ // order variants
38
+ messageWithSortedKeys["variants"] = messageWithSortedKeys["variants"]
39
+ .sort((variantA: Variant, variantB: Variant) => {
40
+ // compare by language
36
41
  const languageComparison = variantA.languageTag.localeCompare(variantB.languageTag)
37
42
 
38
- // If languages are the same, compare by match
43
+ // if languages are the same, compare by match
39
44
  if (languageComparison === 0) {
40
45
  return variantA.match.join("-").localeCompare(variantB.match.join("-"))
41
46
  }
42
47
 
43
48
  return languageComparison
44
- }
45
- )
49
+ })
50
+ // order keys in each variant
51
+ .map((variant: Variant) => {
52
+ const variantWithSortedKeys: any = {}
53
+ for (const variantKey of Object.keys(variant).sort()) {
54
+ if (variantKey === "pattern") {
55
+ variantWithSortedKeys[variantKey] = (variant as any)["pattern"].map((token: any) => {
56
+ const tokenWithSortedKey: any = {}
57
+ for (const tokenKey of Object.keys(token).sort()) {
58
+ tokenWithSortedKey[tokenKey] = token[tokenKey]
59
+ }
60
+ return tokenWithSortedKey
61
+ })
62
+ } else {
63
+ variantWithSortedKeys[variantKey] = (variant as any)[variantKey]
64
+ }
65
+ }
66
+ return variantWithSortedKeys
67
+ })
46
68
 
47
- return JSON.stringify(messageWithSortedKeys, undefined, 4)
69
+ return messageWithSortedKeys as Message
70
+ }
71
+
72
+ export function stringifyMessage(message: Message) {
73
+ return JSON.stringify(normalizeMessage(message), undefined, 4)
48
74
  }
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect } from "vitest"
2
+ // import { parseLixUri, parseOrigin } from "./helpers.js"
3
+ import { normalizeMessage, stringifyMessage } from "./helper.js"
4
+ import type { Message } from "@inlang/message"
5
+
6
+ const unsortedMessageRaw: Message = {
7
+ alias: {},
8
+ selectors: [],
9
+ id: "footer_categories_apps",
10
+ variants: [
11
+ { languageTag: "a", match: ["*", "1"], pattern: [{ type: "Text", value: "2" }] },
12
+ { languageTag: "a", match: ["*", "*"], pattern: [{ type: "Text", value: "1" }] },
13
+ {
14
+ languageTag: "a",
15
+ match: ["1", "*"],
16
+ pattern: [
17
+ { type: "Text", value: "2" },
18
+ { type: "Text", value: "2" },
19
+ ],
20
+ },
21
+ { languageTag: "b", match: [], pattern: [{ type: "Text", value: "4" }] },
22
+ { languageTag: "a", match: ["1", "1"], pattern: [{ type: "Text", value: "2" }] },
23
+ { languageTag: "c", match: [], pattern: [{ value: "5", type: "Text" }] },
24
+ { match: [], languageTag: "d", pattern: [{ type: "Text", value: "6" }] },
25
+ { languageTag: "e", match: [], pattern: [{ type: "Text", value: "7" }] },
26
+ { languageTag: "f", match: [], pattern: [{ type: "Text", value: "8" }] },
27
+ { languageTag: "g", match: [], pattern: [{ type: "Text", value: "9" }] },
28
+ ],
29
+ }
30
+
31
+ const sortedMessageRaw: Message = {
32
+ alias: {},
33
+ id: "footer_categories_apps",
34
+ selectors: [],
35
+ variants: [
36
+ { languageTag: "a", match: ["*", "*"], pattern: [{ type: "Text", value: "1" }] },
37
+ { languageTag: "a", match: ["*", "1"], pattern: [{ type: "Text", value: "2" }] },
38
+ {
39
+ languageTag: "a",
40
+ match: ["1", "*"],
41
+ pattern: [
42
+ { type: "Text", value: "2" },
43
+ { type: "Text", value: "2" },
44
+ ],
45
+ },
46
+ { languageTag: "a", match: ["1", "1"], pattern: [{ type: "Text", value: "2" }] },
47
+ { languageTag: "b", match: [], pattern: [{ type: "Text", value: "4" }] },
48
+ { languageTag: "c", match: [], pattern: [{ type: "Text", value: "5" }] },
49
+ { languageTag: "d", match: [], pattern: [{ type: "Text", value: "6" }] },
50
+ { languageTag: "e", match: [], pattern: [{ type: "Text", value: "7" }] },
51
+ { languageTag: "f", match: [], pattern: [{ type: "Text", value: "8" }] },
52
+ { languageTag: "g", match: [], pattern: [{ type: "Text", value: "9" }] },
53
+ ],
54
+ }
55
+
56
+ // stringify with no indentation
57
+ function str(obj: any) {
58
+ return JSON.stringify(obj)
59
+ }
60
+
61
+ // stringify with 2 space indentation
62
+ function str2(obj: any) {
63
+ return JSON.stringify(obj, undefined, 2)
64
+ }
65
+
66
+ // stringify with 4 space indentation
67
+ function str4(obj: any) {
68
+ return JSON.stringify(obj, undefined, 4)
69
+ }
70
+
71
+ describe("normalizeMessage", () => {
72
+ it("should return the message with sorted keys and variants", () => {
73
+ // test cases are not the same (deep equal) before normalization
74
+ // array order of variants is different
75
+ expect(unsortedMessageRaw).not.toEqual(sortedMessageRaw)
76
+
77
+ // test cases are the same after normalization
78
+ expect(normalizeMessage(unsortedMessageRaw)).toEqual(sortedMessageRaw)
79
+
80
+ // stringify results are not the same before normalization
81
+ expect(str(unsortedMessageRaw)).not.toBe(str(sortedMessageRaw))
82
+
83
+ // stringify results are the same after normalization
84
+ expect(str(normalizeMessage(unsortedMessageRaw))).toBe(str(sortedMessageRaw))
85
+ expect(str2(normalizeMessage(unsortedMessageRaw))).toBe(str2(sortedMessageRaw))
86
+ expect(str4(normalizeMessage(unsortedMessageRaw))).toBe(str4(sortedMessageRaw))
87
+ })
88
+ })
89
+
90
+ describe("stringifyMessage", () => {
91
+ it("should normalize and JSON stringify a message with 4 space indentation", () => {
92
+ expect(stringifyMessage(unsortedMessageRaw)).toBe(str4(sortedMessageRaw))
93
+ expect(stringifyMessage(sortedMessageRaw)).toBe(str4(sortedMessageRaw))
94
+ })
95
+ })