@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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/loadProject.d.ts.map +1 -1
- package/dist/loadProject.js +16 -15
- package/dist/storage/helper.d.ts +24 -0
- package/dist/storage/helper.d.ts.map +1 -1
- package/dist/storage/helper.js +35 -7
- package/dist/storage/helpers.test.d.ts +2 -0
- package/dist/storage/helpers.test.d.ts.map +1 -0
- package/dist/storage/helpers.test.js +84 -0
- package/package.json +6 -6
- package/src/index.ts +1 -0
- package/src/loadProject.ts +20 -15
- package/src/migrations/maybeCreateFirstProjectId.test.ts +1 -1
- package/src/storage/helper.ts +36 -10
- package/src/storage/helpers.test.ts +95 -0
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/loadProject.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
745
|
+
debugLock(lockOrigin + " WARNING - the lock was released by a different process");
|
|
745
746
|
return;
|
|
746
747
|
}
|
|
747
|
-
|
|
748
|
+
debugLock(statError);
|
|
748
749
|
throw statError;
|
|
749
750
|
}
|
|
750
751
|
}
|
package/dist/storage/helper.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/storage/helper.js
CHANGED
|
@@ -15,21 +15,49 @@ export function getPathFromMessageId(id) {
|
|
|
15
15
|
const path = id.replace("_", "/") + fileExtension;
|
|
16
16
|
return path;
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
//
|
|
25
|
-
messageWithSortedKeys["variants"] = messageWithSortedKeys["variants"]
|
|
26
|
-
|
|
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
|
-
//
|
|
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
|
|
59
|
+
return messageWithSortedKeys;
|
|
60
|
+
}
|
|
61
|
+
export function stringifyMessage(message) {
|
|
62
|
+
return JSON.stringify(normalizeMessage(message), undefined, 4);
|
|
35
63
|
}
|
|
@@ -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.
|
|
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/
|
|
39
|
-
"@inlang/
|
|
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
|
-
"@
|
|
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
package/src/loadProject.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1033
|
+
debugLock(lockOrigin + " WARNING - the lock was released by a different process")
|
|
1029
1034
|
return
|
|
1030
1035
|
}
|
|
1031
|
-
|
|
1036
|
+
debugLock(statError)
|
|
1032
1037
|
throw statError
|
|
1033
1038
|
}
|
|
1034
1039
|
}
|
package/src/storage/helper.ts
CHANGED
|
@@ -22,27 +22,53 @@ export function getPathFromMessageId(id: string) {
|
|
|
22
22
|
return path
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
//
|
|
33
|
-
messageWithSortedKeys["variants"] = messageWithSortedKeys["variants"]
|
|
34
|
-
(variantA: Variant, variantB: Variant) => {
|
|
35
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
+
})
|