@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.
Files changed (50) hide show
  1. package/dist/createNewProject.d.ts +17 -0
  2. package/dist/createNewProject.d.ts.map +1 -0
  3. package/dist/createNewProject.js +22 -0
  4. package/dist/createNewProject.test.d.ts +2 -0
  5. package/dist/createNewProject.test.d.ts.map +1 -0
  6. package/dist/createNewProject.test.js +93 -0
  7. package/dist/createNodeishFsWithAbsolutePaths.js +1 -1
  8. package/dist/defaultProjectSettings.d.ts +14 -0
  9. package/dist/defaultProjectSettings.d.ts.map +1 -0
  10. package/dist/defaultProjectSettings.js +23 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +3 -0
  14. package/dist/loadProject.d.ts.map +1 -1
  15. package/dist/loadProject.js +19 -23
  16. package/dist/loadProject.test.js +4 -4
  17. package/dist/migrations/maybeCreateFirstProjectId.test.js +3 -6
  18. package/dist/storage/helper.d.ts +24 -0
  19. package/dist/storage/helper.d.ts.map +1 -1
  20. package/dist/storage/helper.js +35 -7
  21. package/dist/storage/helpers.test.d.ts +2 -0
  22. package/dist/storage/helpers.test.d.ts.map +1 -0
  23. package/dist/storage/helpers.test.js +84 -0
  24. package/dist/validateProjectPath.d.ts +23 -0
  25. package/dist/validateProjectPath.d.ts.map +1 -0
  26. package/dist/validateProjectPath.js +52 -0
  27. package/dist/validateProjectPath.test.d.ts +2 -0
  28. package/dist/validateProjectPath.test.d.ts.map +1 -0
  29. package/dist/validateProjectPath.test.js +56 -0
  30. package/package.json +7 -7
  31. package/src/createNewProject.test.ts +108 -0
  32. package/src/createNewProject.ts +31 -0
  33. package/src/createNodeishFsWithAbsolutePaths.ts +1 -1
  34. package/src/defaultProjectSettings.ts +27 -0
  35. package/src/index.ts +3 -0
  36. package/src/loadProject.test.ts +5 -4
  37. package/src/loadProject.ts +22 -28
  38. package/src/migrations/maybeCreateFirstProjectId.test.ts +3 -6
  39. package/src/storage/helper.ts +36 -10
  40. package/src/storage/helpers.test.ts +95 -0
  41. package/src/validateProjectPath.test.ts +68 -0
  42. package/src/validateProjectPath.ts +58 -0
  43. package/dist/isAbsolutePath.d.ts +0 -2
  44. package/dist/isAbsolutePath.d.ts.map +0 -1
  45. package/dist/isAbsolutePath.js +0 -4
  46. package/dist/isAbsolutePath.test.d.ts +0 -2
  47. package/dist/isAbsolutePath.test.d.ts.map +0 -1
  48. package/dist/isAbsolutePath.test.js +0 -20
  49. package/src/isAbsolutePath.test.ts +0 -23
  50. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=createNewProject.test.d.ts.map
@@ -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 "./isAbsolutePath.js";
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";
@@ -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;AAyBhF,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,CAqYzB;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"}
@@ -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, LoadProjectInvalidArgument, PluginLoadMessagesError, } from "./errors.js";
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 { isAbsolutePath } from "./isAbsolutePath.js";
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
- if (!isAbsolutePath(args.projectPath)) {
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
- debug(lockOrigin + " tries to acquire a lockfile Retry Nr.: " + tryCount);
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
- debug(lockOrigin + " acquired a lockfile Retry Nr.: " + tryCount);
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
- 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);
659
655
  return acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1);
660
656
  }
661
657
  throw fstatError;
662
658
  }
663
- debug(lockOrigin +
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
- 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);
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
- debug(lockOrigin +
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
- debug(lockOrigin +
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
- debug(lockOrigin +
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
- debug(lockOrigin + " tryCount++ different locker - try to acquire again " + tryCount);
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
- debug(lockOrigin + " releasing the lock ");
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
- debug(lockOrigin + " couldn't release the lock");
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
- debug(lockOrigin + " WARNING - the lock was released by a different process");
745
+ debugLock(lockOrigin + " WARNING - the lock was released by a different process");
750
746
  return;
751
747
  }
752
- debug(statError);
748
+ debugLock(statError);
753
749
  throw statError;
754
750
  }
755
751
  }
@@ -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 { LoadProjectInvalidArgument, ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, } from "./errors.js";
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.error).toBeInstanceOf(LoadProjectInvalidArgument);
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).toBeInstanceOf(LoadProjectInvalidArgument);
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).toBeInstanceOf(LoadProjectInvalidArgument);
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 { openRepository } from "@lix-js/client/src/openRepository.ts";
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
- const repoWithError = await openRepository("https://github.com/inlang/no-exist", {
20
- nodeishFs: createNodeishMemoryFs(),
21
- });
18
+ await repo.nodeishFs.rm("/.git", { recursive: true });
22
19
  const projectId = await generateProjectId({
23
- repo: repoWithError,
20
+ repo: repo,
24
21
  projectPath: "mocked_project_path",
25
22
  });
26
23
  expect(projectId).toBeUndefined();
@@ -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
+ });