@inlang/sdk 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/createNewProject.d.ts +17 -0
- package/dist/createNewProject.d.ts.map +1 -0
- package/dist/createNewProject.js +22 -0
- package/dist/createNewProject.test.d.ts +2 -0
- package/dist/createNewProject.test.d.ts.map +1 -0
- package/dist/createNewProject.test.js +93 -0
- package/dist/createNodeishFsWithAbsolutePaths.js +1 -1
- package/dist/defaultProjectSettings.d.ts +14 -0
- package/dist/defaultProjectSettings.d.ts.map +1 -0
- package/dist/defaultProjectSettings.js +23 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/loadProject.d.ts.map +1 -1
- package/dist/loadProject.js +19 -23
- package/dist/loadProject.test.js +4 -4
- package/dist/migrations/maybeCreateFirstProjectId.test.js +3 -6
- 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/dist/validateProjectPath.d.ts +23 -0
- package/dist/validateProjectPath.d.ts.map +1 -0
- package/dist/validateProjectPath.js +52 -0
- package/dist/validateProjectPath.test.d.ts +2 -0
- package/dist/validateProjectPath.test.d.ts.map +1 -0
- package/dist/validateProjectPath.test.js +56 -0
- package/package.json +7 -7
- package/src/createNewProject.test.ts +108 -0
- package/src/createNewProject.ts +31 -0
- package/src/createNodeishFsWithAbsolutePaths.ts +1 -1
- package/src/defaultProjectSettings.ts +27 -0
- package/src/index.ts +3 -0
- package/src/loadProject.test.ts +5 -4
- package/src/loadProject.ts +22 -28
- package/src/migrations/maybeCreateFirstProjectId.test.ts +3 -6
- package/src/storage/helper.ts +36 -10
- package/src/storage/helpers.test.ts +95 -0
- package/src/validateProjectPath.test.ts +68 -0
- package/src/validateProjectPath.ts +58 -0
- package/dist/isAbsolutePath.d.ts +0 -2
- package/dist/isAbsolutePath.d.ts.map +0 -1
- package/dist/isAbsolutePath.js +0 -4
- package/dist/isAbsolutePath.test.d.ts +0 -2
- package/dist/isAbsolutePath.test.d.ts.map +0 -1
- package/dist/isAbsolutePath.test.js +0 -20
- package/src/isAbsolutePath.test.ts +0 -23
- package/src/isAbsolutePath.ts +0 -5
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Repository } from "@lix-js/client";
|
|
2
|
+
import { ProjectSettings } from "@inlang/project-settings";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a new project in the given directory.
|
|
5
|
+
* The directory must be an absolute path, must not exist, and must end with {name}.inlang
|
|
6
|
+
* Uses defaultProjectSettings unless projectSettings are provided.
|
|
7
|
+
*
|
|
8
|
+
* @param projectPath - Absolute path to the [name].inlang directory
|
|
9
|
+
* @param repo - An instance of a lix repo as returned by `openRepository`
|
|
10
|
+
* @param projectSettings - Optional project settings to use for the new project.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createNewProject(args: {
|
|
13
|
+
projectPath: string;
|
|
14
|
+
repo: Repository;
|
|
15
|
+
projectSettings: ProjectSettings;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
//# sourceMappingURL=createNewProject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createNewProject.d.ts","sourceRoot":"","sources":["../src/createNewProject.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAI1D;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC5C,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,UAAU,CAAA;IAChB,eAAe,EAAE,eAAe,CAAA;CAChC,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ProjectSettings } from "@inlang/project-settings";
|
|
2
|
+
import { assertValidProjectPath, pathExists } from "./validateProjectPath.js";
|
|
3
|
+
import { defaultProjectSettings } from "./defaultProjectSettings.js";
|
|
4
|
+
/**
|
|
5
|
+
* Creates a new project in the given directory.
|
|
6
|
+
* The directory must be an absolute path, must not exist, and must end with {name}.inlang
|
|
7
|
+
* Uses defaultProjectSettings unless projectSettings are provided.
|
|
8
|
+
*
|
|
9
|
+
* @param projectPath - Absolute path to the [name].inlang directory
|
|
10
|
+
* @param repo - An instance of a lix repo as returned by `openRepository`
|
|
11
|
+
* @param projectSettings - Optional project settings to use for the new project.
|
|
12
|
+
*/
|
|
13
|
+
export async function createNewProject(args) {
|
|
14
|
+
assertValidProjectPath(args.projectPath);
|
|
15
|
+
const nodeishFs = args.repo.nodeishFs;
|
|
16
|
+
if (await pathExists(args.projectPath, nodeishFs)) {
|
|
17
|
+
throw new Error(`projectPath already exists, received "${args.projectPath}"`);
|
|
18
|
+
}
|
|
19
|
+
await nodeishFs.mkdir(args.projectPath, { recursive: true });
|
|
20
|
+
const settingsText = JSON.stringify(args.projectSettings ?? defaultProjectSettings, undefined, 2);
|
|
21
|
+
await nodeishFs.writeFile(`${args.projectPath}/settings.json`, settingsText);
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createNewProject.test.d.ts","sourceRoot":"","sources":["../src/createNewProject.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createNewProject } from "./createNewProject.js";
|
|
3
|
+
import { mockRepo } from "@lix-js/client";
|
|
4
|
+
import { defaultProjectSettings } from "./defaultProjectSettings.js";
|
|
5
|
+
import { loadProject } from "./loadProject.js";
|
|
6
|
+
import { createMessage } from "./test-utilities/createMessage.js";
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
describe("createNewProject", () => {
|
|
11
|
+
it("should throw if a path does not end with .inlang", async () => {
|
|
12
|
+
const repo = await mockRepo();
|
|
13
|
+
const projectPath = "/test/project.inl";
|
|
14
|
+
try {
|
|
15
|
+
await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
|
|
16
|
+
// should not reach this point
|
|
17
|
+
throw new Error("Expected an error");
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
expect(e.message).toMatch('Expected a path ending in "{name}.inlang" but received "/test/project.inl"');
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
it("should throw if projectPath is not an absolute path", async () => {
|
|
24
|
+
const repo = await mockRepo();
|
|
25
|
+
const projectPath = "test/project.inlang";
|
|
26
|
+
try {
|
|
27
|
+
await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
|
|
28
|
+
// should not reach this point
|
|
29
|
+
throw new Error("Expected an error");
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
expect(e.message).toMatch('Expected an absolute path but received "test/project.inlang"');
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
it("should throw if the path already exists", async () => {
|
|
36
|
+
const repo = await mockRepo();
|
|
37
|
+
const projectPath = "/test/project.inlang";
|
|
38
|
+
await repo.nodeishFs.mkdir(projectPath, { recursive: true });
|
|
39
|
+
try {
|
|
40
|
+
await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
|
|
41
|
+
// should not reach this point
|
|
42
|
+
throw new Error("Expected an error");
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
expect(e.message).toMatch('projectPath already exists, received "/test/project.inlang"');
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
it("should create default defaultProjectSettings in projectPath", async () => {
|
|
49
|
+
const repo = await mockRepo();
|
|
50
|
+
const projectPath = "/test/project.inlang";
|
|
51
|
+
await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
|
|
52
|
+
const json = await repo.nodeishFs.readFile(`${projectPath}/settings.json`, {
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
});
|
|
55
|
+
const settings = JSON.parse(json);
|
|
56
|
+
expect(settings).toEqual(defaultProjectSettings);
|
|
57
|
+
});
|
|
58
|
+
it("should create different projectSettings in projectPath", async () => {
|
|
59
|
+
const repo = await mockRepo();
|
|
60
|
+
const projectPath = "/test/project.inlang";
|
|
61
|
+
const projectSettings = { ...defaultProjectSettings, languageTags: ["en", "de", "fr"] };
|
|
62
|
+
await createNewProject({ projectPath, repo, projectSettings });
|
|
63
|
+
const json = await repo.nodeishFs.readFile(`${projectPath}/settings.json`, {
|
|
64
|
+
encoding: "utf-8",
|
|
65
|
+
});
|
|
66
|
+
const settings = JSON.parse(json);
|
|
67
|
+
expect(settings).toEqual(projectSettings);
|
|
68
|
+
expect(settings).not.toEqual(defaultProjectSettings);
|
|
69
|
+
});
|
|
70
|
+
it("should load the project after creating it", async () => {
|
|
71
|
+
const repo = await mockRepo();
|
|
72
|
+
const projectPath = "/test/project.inlang";
|
|
73
|
+
await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
|
|
74
|
+
const project = await loadProject({ projectPath, repo });
|
|
75
|
+
expect(project.errors().length).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
it("should create messages inside the project directory", async () => {
|
|
78
|
+
const repo = await mockRepo();
|
|
79
|
+
const projectPath = "/test/project.inlang";
|
|
80
|
+
await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
|
|
81
|
+
const project = await loadProject({ projectPath, repo });
|
|
82
|
+
expect(project.errors().length).toBe(0);
|
|
83
|
+
const testMessage = createMessage("test", { en: "test message" });
|
|
84
|
+
project.query.messages.create({ data: testMessage });
|
|
85
|
+
const messages = project.query.messages.getAll();
|
|
86
|
+
expect(messages.length).toBe(1);
|
|
87
|
+
expect(messages[0]).toEqual(testMessage);
|
|
88
|
+
await sleep(20);
|
|
89
|
+
const json = await repo.nodeishFs.readFile("/test/messages/en.json", { encoding: "utf-8" });
|
|
90
|
+
const jsonMessages = JSON.parse(json);
|
|
91
|
+
expect(jsonMessages["test"]).toEqual("test message");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { normalizePath } from "@lix-js/fs";
|
|
2
|
-
import { isAbsolutePath } from "./
|
|
2
|
+
import { isAbsolutePath } from "./validateProjectPath.js";
|
|
3
3
|
/**
|
|
4
4
|
* Wraps the nodeish filesystem subset with a function that intercepts paths
|
|
5
5
|
* and prepends the base path.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default project settings for createNewProject
|
|
3
|
+
* from paraglide-js/src/cli/commands/init/defaults.ts
|
|
4
|
+
*/
|
|
5
|
+
export declare const defaultProjectSettings: {
|
|
6
|
+
$schema: "https://inlang.com/schema/project-settings";
|
|
7
|
+
sourceLanguageTag: string;
|
|
8
|
+
languageTags: string[];
|
|
9
|
+
modules: string[];
|
|
10
|
+
"plugin.inlang.messageFormat": {
|
|
11
|
+
pathPattern: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=defaultProjectSettings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaultProjectSettings.d.ts","sourceRoot":"","sources":["../src/defaultProjectSettings.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;CAoBR,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default project settings for createNewProject
|
|
3
|
+
* from paraglide-js/src/cli/commands/init/defaults.ts
|
|
4
|
+
*/
|
|
5
|
+
export const defaultProjectSettings = {
|
|
6
|
+
$schema: "https://inlang.com/schema/project-settings",
|
|
7
|
+
sourceLanguageTag: "en",
|
|
8
|
+
languageTags: ["en"],
|
|
9
|
+
modules: [
|
|
10
|
+
// for instant gratification, we're adding common rules
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js",
|
|
13
|
+
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js",
|
|
14
|
+
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@latest/dist/index.js",
|
|
15
|
+
// default to the message format plugin because it supports all features
|
|
16
|
+
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js",
|
|
17
|
+
// the m function matcher should be installed by default in case Sherlock (VS Code extension) is adopted
|
|
18
|
+
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js",
|
|
19
|
+
],
|
|
20
|
+
"plugin.inlang.messageFormat": {
|
|
21
|
+
pathPattern: "./messages/{languageTag}.json",
|
|
22
|
+
},
|
|
23
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export type { InlangProject, InstalledMessageLintRule, InstalledPlugin, MessageQueryApi, Subscribable, } from "./api.js";
|
|
7
7
|
export { type ImportFunction, createImport } from "./resolve-modules/index.js";
|
|
8
|
+
export { createNewProject } from "./createNewProject.js";
|
|
9
|
+
export { defaultProjectSettings } from "./defaultProjectSettings.js";
|
|
8
10
|
export { loadProject } from "./loadProject.js";
|
|
9
11
|
export { listProjects } from "./listProjects.js";
|
|
10
12
|
export { solidAdapter, type InlangProjectWithSolidAdapter } from "./adapter/solidAdapter.js";
|
|
11
13
|
export { createMessagesQuery } from "./createMessagesQuery.js";
|
|
12
14
|
export { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginLoadMessagesError, PluginSaveMessagesError, } from "./errors.js";
|
|
15
|
+
export { normalizeMessage } from "./storage/helper.js";
|
|
13
16
|
export * from "./messages/variant.js";
|
|
14
17
|
export * from "./versionedInterfaces.js";
|
|
15
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,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
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
*! EXPORT AS LITTLE AS POSSIBLE TO MINIMIZE THE CHANCE OF BREAKING CHANGES.
|
|
5
5
|
*/
|
|
6
6
|
export { createImport } from "./resolve-modules/index.js";
|
|
7
|
+
export { createNewProject } from "./createNewProject.js";
|
|
8
|
+
export { defaultProjectSettings } from "./defaultProjectSettings.js";
|
|
7
9
|
export { loadProject } from "./loadProject.js";
|
|
8
10
|
export { listProjects } from "./listProjects.js";
|
|
9
11
|
export { solidAdapter } from "./adapter/solidAdapter.js";
|
|
10
12
|
export { createMessagesQuery } from "./createMessagesQuery.js";
|
|
11
13
|
export { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginLoadMessagesError, PluginSaveMessagesError, } from "./errors.js";
|
|
14
|
+
export { normalizeMessage } from "./storage/helper.js";
|
|
12
15
|
export * from "./messages/variant.js";
|
|
13
16
|
export * from "./versionedInterfaces.js";
|
|
14
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;
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveModules } from "./resolve-modules/index.js";
|
|
2
2
|
import { TypeCompiler, ValueErrorType } from "@sinclair/typebox/compiler";
|
|
3
|
-
import { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginSaveMessagesError,
|
|
3
|
+
import { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginSaveMessagesError, PluginLoadMessagesError, } from "./errors.js";
|
|
4
4
|
import { createRoot, createSignal, createEffect } from "./reactivity/solid.js";
|
|
5
5
|
import { createMessagesQuery } from "./createMessagesQuery.js";
|
|
6
6
|
import { createMessageLintReportsQuery } from "./createMessageLintReportsQuery.js";
|
|
@@ -9,7 +9,7 @@ import { tryCatch } from "@inlang/result";
|
|
|
9
9
|
import { migrateIfOutdated } from "@inlang/project-settings/migration";
|
|
10
10
|
import { createNodeishFsWithAbsolutePaths } from "./createNodeishFsWithAbsolutePaths.js";
|
|
11
11
|
import { normalizePath } from "@lix-js/fs";
|
|
12
|
-
import {
|
|
12
|
+
import { assertValidProjectPath } from "./validateProjectPath.js";
|
|
13
13
|
import { maybeMigrateToDirectory } from "./migrations/migrateToDirectory.js";
|
|
14
14
|
import { stringifyMessage as stringifyMessage } from "./storage/helper.js";
|
|
15
15
|
import { humanIdHash } from "./storage/human-id/human-readable-id.js";
|
|
@@ -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.
|
|
@@ -43,12 +44,7 @@ export async function loadProject(args) {
|
|
|
43
44
|
// the only place where throwing is acceptable because the project
|
|
44
45
|
// won't even be loaded. do not throw anywhere else. otherwise, apps
|
|
45
46
|
// can't handle errors gracefully.
|
|
46
|
-
|
|
47
|
-
throw new LoadProjectInvalidArgument(`Expected an absolute path but received "${args.projectPath}".`, { argument: "projectPath" });
|
|
48
|
-
}
|
|
49
|
-
else if (/[^\\/]+\.inlang$/.test(projectPath) === false) {
|
|
50
|
-
throw new LoadProjectInvalidArgument(`Expected a path ending in "{name}.inlang" but received "${projectPath}".\n\nValid examples: \n- "/path/to/micky-mouse.inlang"\n- "/path/to/green-elephant.inlang\n`, { argument: "projectPath" });
|
|
51
|
-
}
|
|
47
|
+
assertValidProjectPath(projectPath);
|
|
52
48
|
const nodeishFs = createNodeishFsWithAbsolutePaths({
|
|
53
49
|
projectPath,
|
|
54
50
|
nodeishFs: args.repo.nodeishFs,
|
|
@@ -454,7 +450,7 @@ async function loadMessagesViaPlugin(fs, lockDirPath, messageState, messagesQuer
|
|
|
454
450
|
const importedEnecoded = stringifyMessage(loadedMessageClone);
|
|
455
451
|
// NOTE could use hash instead of the whole object JSON to save memory...
|
|
456
452
|
if (messageState.messageLoadHash[loadedMessageClone.id] === importedEnecoded) {
|
|
457
|
-
debug("skipping upsert!")
|
|
453
|
+
// debug("skipping upsert!")
|
|
458
454
|
continue;
|
|
459
455
|
}
|
|
460
456
|
// This logic is preventing cycles - could also be handled if update api had a parameter for who triggered update
|
|
@@ -635,10 +631,10 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
|
|
|
635
631
|
throw new Error(lockOrigin + " exceeded maximum Retries (5) to acquire lockfile " + tryCount);
|
|
636
632
|
}
|
|
637
633
|
try {
|
|
638
|
-
|
|
634
|
+
debugLock(lockOrigin + " tries to acquire a lockfile Retry Nr.: " + tryCount);
|
|
639
635
|
await fs.mkdir(lockDirPath);
|
|
640
636
|
const stats = await fs.stat(lockDirPath);
|
|
641
|
-
|
|
637
|
+
debugLock(lockOrigin + " acquired a lockfile Retry Nr.: " + tryCount);
|
|
642
638
|
return stats.mtimeMs;
|
|
643
639
|
}
|
|
644
640
|
catch (error) {
|
|
@@ -655,12 +651,12 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
|
|
|
655
651
|
catch (fstatError) {
|
|
656
652
|
if (fstatError.code === "ENOENT") {
|
|
657
653
|
// lock file seems to be gone :) - lets try again
|
|
658
|
-
|
|
654
|
+
debugLock(lockOrigin + " tryCount++ lock file seems to be gone :) - lets try again " + tryCount);
|
|
659
655
|
return acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1);
|
|
660
656
|
}
|
|
661
657
|
throw fstatError;
|
|
662
658
|
}
|
|
663
|
-
|
|
659
|
+
debugLock(lockOrigin +
|
|
664
660
|
" tries to acquire a lockfile - lock currently in use... starting probe phase " +
|
|
665
661
|
tryCount);
|
|
666
662
|
return new Promise((resolve, reject) => {
|
|
@@ -670,13 +666,13 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
|
|
|
670
666
|
probeCounts += 1;
|
|
671
667
|
let lockFileStats = undefined;
|
|
672
668
|
try {
|
|
673
|
-
|
|
669
|
+
debugLock(lockOrigin + " tries to acquire a lockfile - check if the lock is free now " + tryCount);
|
|
674
670
|
// alright lets give it another try
|
|
675
671
|
lockFileStats = await fs.stat(lockDirPath);
|
|
676
672
|
}
|
|
677
673
|
catch (fstatError) {
|
|
678
674
|
if (fstatError.code === "ENOENT") {
|
|
679
|
-
|
|
675
|
+
debugLock(lockOrigin +
|
|
680
676
|
" tryCount++ in Promise - tries to acquire a lockfile - lock file seems to be free now - try to acquire " +
|
|
681
677
|
tryCount);
|
|
682
678
|
const lock = acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1);
|
|
@@ -688,7 +684,7 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
|
|
|
688
684
|
if (lockFileStats.mtimeMs === currentLockTime) {
|
|
689
685
|
if (probeCounts >= nProbes) {
|
|
690
686
|
// ok maximum lock time ran up (we waitetd nProbes * probeInterval) - we consider the lock to be stale
|
|
691
|
-
|
|
687
|
+
debugLock(lockOrigin +
|
|
692
688
|
" tries to acquire a lockfile - lock not free - but stale lets drop it" +
|
|
693
689
|
tryCount);
|
|
694
690
|
try {
|
|
@@ -703,7 +699,7 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
|
|
|
703
699
|
return reject(rmLockError);
|
|
704
700
|
}
|
|
705
701
|
try {
|
|
706
|
-
|
|
702
|
+
debugLock(lockOrigin +
|
|
707
703
|
" tryCount++ same locker - try to acquire again after removing stale lock " +
|
|
708
704
|
tryCount);
|
|
709
705
|
const lock = await acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1);
|
|
@@ -720,7 +716,7 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
|
|
|
720
716
|
}
|
|
721
717
|
else {
|
|
722
718
|
try {
|
|
723
|
-
|
|
719
|
+
debugLock(lockOrigin + " tryCount++ different locker - try to acquire again " + tryCount);
|
|
724
720
|
const lock = await acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1);
|
|
725
721
|
return resolve(lock);
|
|
726
722
|
}
|
|
@@ -734,7 +730,7 @@ async function acquireFileLock(fs, lockDirPath, lockOrigin, tryCount = 0) {
|
|
|
734
730
|
});
|
|
735
731
|
}
|
|
736
732
|
async function releaseLock(fs, lockDirPath, lockOrigin, lockTime) {
|
|
737
|
-
|
|
733
|
+
debugLock(lockOrigin + " releasing the lock ");
|
|
738
734
|
try {
|
|
739
735
|
const stats = await fs.stat(lockDirPath);
|
|
740
736
|
if (stats.mtimeMs === lockTime) {
|
|
@@ -743,13 +739,13 @@ async function releaseLock(fs, lockDirPath, lockOrigin, lockTime) {
|
|
|
743
739
|
}
|
|
744
740
|
}
|
|
745
741
|
catch (statError) {
|
|
746
|
-
|
|
742
|
+
debugLock(lockOrigin + " couldn't release the lock");
|
|
747
743
|
if (statError.code === "ENOENT") {
|
|
748
744
|
// ok seeks like the log was released by someone else
|
|
749
|
-
|
|
745
|
+
debugLock(lockOrigin + " WARNING - the lock was released by a different process");
|
|
750
746
|
return;
|
|
751
747
|
}
|
|
752
|
-
|
|
748
|
+
debugLock(statError);
|
|
753
749
|
throw statError;
|
|
754
750
|
}
|
|
755
751
|
}
|
package/dist/loadProject.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
2
|
import { describe, it, expect, vi } from "vitest";
|
|
3
3
|
import { loadProject } from "./loadProject.js";
|
|
4
|
-
import {
|
|
4
|
+
import { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, } from "./errors.js";
|
|
5
5
|
import { normalizePath } from "@lix-js/fs";
|
|
6
6
|
import { createMessage } from "./test-utilities/createMessage.js";
|
|
7
7
|
import { tryCatch } from "@inlang/result";
|
|
@@ -144,7 +144,7 @@ it("should throw if a project (path) does not have a name", async () => {
|
|
|
144
144
|
repo,
|
|
145
145
|
_import,
|
|
146
146
|
}));
|
|
147
|
-
expect(project
|
|
147
|
+
expect(project?.error?.message).toMatch('Expected a path ending in "{name}.inlang" but received ');
|
|
148
148
|
});
|
|
149
149
|
it("should throw if a project path does not end with .inlang", async () => {
|
|
150
150
|
const repo = await mockRepo();
|
|
@@ -159,7 +159,7 @@ it("should throw if a project path does not end with .inlang", async () => {
|
|
|
159
159
|
repo,
|
|
160
160
|
_import,
|
|
161
161
|
}));
|
|
162
|
-
expect(project.error).
|
|
162
|
+
expect(project.error?.message).toMatch('Expected a path ending in "{name}.inlang" but received ');
|
|
163
163
|
}
|
|
164
164
|
});
|
|
165
165
|
describe("initialization", () => {
|
|
@@ -170,7 +170,7 @@ describe("initialization", () => {
|
|
|
170
170
|
repo,
|
|
171
171
|
_import,
|
|
172
172
|
}));
|
|
173
|
-
expect(result.error).
|
|
173
|
+
expect(result.error?.message).toBe('Expected an absolute path but received "relative/path".');
|
|
174
174
|
expect(result.data).toBeUndefined();
|
|
175
175
|
});
|
|
176
176
|
it("should generate projectId on missing projectid", async () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { generateProjectId } from "./maybeCreateFirstProjectId.js";
|
|
2
2
|
import { it, expect } from "vitest";
|
|
3
|
-
import {
|
|
4
|
-
import { mockRepo, createNodeishMemoryFs } from "@lix-js/client";
|
|
3
|
+
import { mockRepo } from "@lix-js/client";
|
|
5
4
|
import {} from "@lix-js/fs";
|
|
6
5
|
// eslint-disable-next-line no-restricted-imports -- test
|
|
7
6
|
import { readFileSync } from "node:fs";
|
|
@@ -16,11 +15,9 @@ it("should generate a project id", async () => {
|
|
|
16
15
|
expect(projectId).toBe("432d7ef29c510e99d95e2d14ef57a0797a1603859b5a851b7dff7e77161b8c08");
|
|
17
16
|
});
|
|
18
17
|
it("should return undefined if repoMeta contains error", async () => {
|
|
19
|
-
|
|
20
|
-
nodeishFs: createNodeishMemoryFs(),
|
|
21
|
-
});
|
|
18
|
+
await repo.nodeishFs.rm("/.git", { recursive: true });
|
|
22
19
|
const projectId = await generateProjectId({
|
|
23
|
-
repo:
|
|
20
|
+
repo: repo,
|
|
24
21
|
projectPath: "mocked_project_path",
|
|
25
22
|
});
|
|
26
23
|
expect(projectId).toBeUndefined();
|
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
|
+
});
|