@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,23 @@
1
+ import type { NodeishFilesystem } from "@lix-js/fs";
2
+ /**
3
+ * validate that a project path is absolute and ends with {name}.inlang.
4
+ *
5
+ * @throws if the path is not valid.
6
+ */
7
+ export declare function assertValidProjectPath(projectPath: string): void;
8
+ /**
9
+ * tests whether a path ends with {name}.inlang
10
+ * (does not remove trailing slash)
11
+ */
12
+ export declare function isInlangProjectPath(path: string): boolean;
13
+ /**
14
+ * tests whether a path starts with a forward slash (/) or a windows-style
15
+ * drive letter (C: or D:, etc.) followed by a slash
16
+ */
17
+ export declare function isAbsolutePath(path: string): boolean;
18
+ /**
19
+ * Returns true if the path exists (file or directory), false otherwise.
20
+ *
21
+ */
22
+ export declare function pathExists(filePath: string, nodeishFs: NodeishFilesystem): Promise<boolean>;
23
+ //# sourceMappingURL=validateProjectPath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateProjectPath.d.ts","sourceRoot":"","sources":["../src/validateProjectPath.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,QASzD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,WAE/C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,WAO1C;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,iBAAiB,oBAc9E"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * validate that a project path is absolute and ends with {name}.inlang.
3
+ *
4
+ * @throws if the path is not valid.
5
+ */
6
+ export function assertValidProjectPath(projectPath) {
7
+ if (!isAbsolutePath(projectPath)) {
8
+ throw new Error(`Expected an absolute path but received "${projectPath}".`);
9
+ }
10
+ if (!isInlangProjectPath(projectPath)) {
11
+ throw new Error(`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`);
12
+ }
13
+ }
14
+ /**
15
+ * tests whether a path ends with {name}.inlang
16
+ * (does not remove trailing slash)
17
+ */
18
+ export function isInlangProjectPath(path) {
19
+ return /[^\\/]+\.inlang$/.test(path);
20
+ }
21
+ /**
22
+ * tests whether a path starts with a forward slash (/) or a windows-style
23
+ * drive letter (C: or D:, etc.) followed by a slash
24
+ */
25
+ export function isAbsolutePath(path) {
26
+ return /^\/|^[A-Za-z]:[\\/]/.test(path);
27
+ // OG from sdk/src/isAbsolutePath.ts - TODO: find out where this regex came from
28
+ // const matchPosixAndWindowsAbsolutePaths =
29
+ // /^(?:[A-Za-z]:\\(?:[^\\]+\\)*[^\\]+|[A-Za-z]:\/(?:[^/]+\/)*[^/]+|\/(?:[^/]+\/)*[^/]+)$/
30
+ // return matchPosixAndWindowsAbsolutePaths.test(path)
31
+ }
32
+ /**
33
+ * Returns true if the path exists (file or directory), false otherwise.
34
+ *
35
+ */
36
+ export async function pathExists(filePath, nodeishFs) {
37
+ // from paraglide-js/src/services/file-handling/exists.ts
38
+ // TODO: add fs.exists to @lix-js/fs
39
+ try {
40
+ await nodeishFs.stat(filePath);
41
+ return true;
42
+ }
43
+ catch (error) {
44
+ //@ts-ignore
45
+ if (error.code === "ENOENT") {
46
+ return false;
47
+ }
48
+ else {
49
+ throw new Error(`Failed to check if path exists: ${error}`, { cause: error });
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=validateProjectPath.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateProjectPath.test.d.ts","sourceRoot":"","sources":["../src/validateProjectPath.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,56 @@
1
+ import { assert, describe, expect, it } from "vitest";
2
+ import { assertValidProjectPath, isAbsolutePath, isInlangProjectPath, pathExists, } from "./validateProjectPath.js";
3
+ import { mockRepo } from "@lix-js/client";
4
+ describe("isAbsolutePath", () => {
5
+ it("should correctly identify Unix absolute paths", () => {
6
+ assert.isTrue(isAbsolutePath("/home/user/documents/file.txt"));
7
+ assert.isTrue(isAbsolutePath("/usr/local/bin/script.sh"));
8
+ assert.isFalse(isAbsolutePath("relative/path/to/file.txt"));
9
+ });
10
+ it("should correctly identify Windows absolute paths", () => {
11
+ assert.isTrue(isAbsolutePath("C:\\Users\\User\\Documents\\File.txt"));
12
+ assert.isTrue(isAbsolutePath("C:/Users/user/project.inlang/settings.json"));
13
+ assert.isFalse(isAbsolutePath("Projects\\Project1\\source\\file.txt"));
14
+ });
15
+ it("should handle edge cases", () => {
16
+ assert.isFalse(isAbsolutePath("")); // Empty path should return false
17
+ assert.isFalse(isAbsolutePath("relative/path/../file.txt")); // Relative path with ".." should return false
18
+ assert.isFalse(isAbsolutePath("../relative/path/to/file.txt"));
19
+ assert.isFalse(isAbsolutePath("./relative/path/to/file.txt"));
20
+ });
21
+ });
22
+ describe("isInlangProjectPath", () => {
23
+ it("should correctly identify valid inlang project paths", () => {
24
+ assert.isTrue(isInlangProjectPath("/path/to/orange-mouse.inlang"));
25
+ assert.isFalse(isInlangProjectPath("relative/path/to/file.txt"));
26
+ assert.isFalse(isInlangProjectPath("/path/to/.inlang"));
27
+ assert.isFalse(isInlangProjectPath("/path/to/white-elephant.inlang/"));
28
+ assert.isFalse(isInlangProjectPath("/path/to/blue-elephant.inlang/settings.json"));
29
+ });
30
+ });
31
+ describe("assertValidProjectPath", () => {
32
+ it("should not throw for valid project paths", () => {
33
+ assert.doesNotThrow(() => assertValidProjectPath("/path/to/brown-mouse.inlang"));
34
+ assert.doesNotThrow(() => assertValidProjectPath("/path/to/green-elephant.inlang"));
35
+ });
36
+ it("should throw for invalid project paths", () => {
37
+ assert.throws(() => assertValidProjectPath("relative/path/to/flying-lizard.inlang"));
38
+ assert.throws(() => assertValidProjectPath("/path/to/loud-mouse.inlang/"));
39
+ assert.throws(() => assertValidProjectPath("/path/to/green-elephant.inlang/settings.json"));
40
+ });
41
+ });
42
+ // moar tests in paraglide-js/src/services/file-handling/exists.test.ts
43
+ describe("pathExists", () => {
44
+ it("should work for files", async () => {
45
+ const repo = await mockRepo();
46
+ await repo.nodeishFs.writeFile("/test.txt", "hello");
47
+ expect(await pathExists("/test.txt", repo.nodeishFs)).toBe(true);
48
+ expect(await pathExists("/does-not-exist.txt", repo.nodeishFs)).toBe(false);
49
+ });
50
+ it("should work for directories", async () => {
51
+ const repo = await mockRepo();
52
+ await repo.nodeishFs.mkdir("/test/project.inlang", { recursive: true });
53
+ expect(await pathExists("/test/project.inlang", repo.nodeishFs)).toBe(true);
54
+ expect(await pathExists("/test/white-gorilla.inlang", repo.nodeishFs)).toBe(false);
55
+ });
56
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inlang/sdk",
3
3
  "type": "module",
4
- "version": "0.32.0",
4
+ "version": "0.34.0",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -32,16 +32,16 @@
32
32
  "murmurhash3js": "^3.0.1",
33
33
  "solid-js": "1.6.12",
34
34
  "throttle-debounce": "^5.0.0",
35
- "@inlang/language-tag": "1.5.1",
36
35
  "@inlang/json-types": "1.1.0",
37
36
  "@inlang/message": "2.1.0",
38
- "@inlang/message-lint-rule": "1.4.5",
39
- "@inlang/module": "1.2.9",
40
- "@inlang/plugin": "2.4.9",
41
- "@inlang/project-settings": "2.4.0",
37
+ "@inlang/message-lint-rule": "1.4.6",
38
+ "@inlang/language-tag": "1.5.1",
39
+ "@inlang/module": "1.2.10",
40
+ "@inlang/project-settings": "2.4.1",
41
+ "@inlang/plugin": "2.4.10",
42
42
  "@inlang/result": "1.1.0",
43
43
  "@inlang/translatable": "1.3.1",
44
- "@lix-js/client": "1.2.0",
44
+ "@lix-js/client": "1.3.0",
45
45
  "@lix-js/fs": "1.0.0"
46
46
  },
47
47
  "devDependencies": {
@@ -0,0 +1,108 @@
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
+
8
+ function sleep(ms: number) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms))
10
+ }
11
+
12
+ describe("createNewProject", () => {
13
+ it("should throw if a path does not end with .inlang", async () => {
14
+ const repo = await mockRepo()
15
+ const projectPath = "/test/project.inl"
16
+ try {
17
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
18
+ // should not reach this point
19
+ throw new Error("Expected an error")
20
+ } catch (e) {
21
+ expect((e as Error).message).toMatch(
22
+ 'Expected a path ending in "{name}.inlang" but received "/test/project.inl"'
23
+ )
24
+ }
25
+ })
26
+
27
+ it("should throw if projectPath is not an absolute path", async () => {
28
+ const repo = await mockRepo()
29
+ const projectPath = "test/project.inlang"
30
+ try {
31
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
32
+ // should not reach this point
33
+ throw new Error("Expected an error")
34
+ } catch (e) {
35
+ expect((e as Error).message).toMatch(
36
+ 'Expected an absolute path but received "test/project.inlang"'
37
+ )
38
+ }
39
+ })
40
+
41
+ it("should throw if the path already exists", async () => {
42
+ const repo = await mockRepo()
43
+ const projectPath = "/test/project.inlang"
44
+ await repo.nodeishFs.mkdir(projectPath, { recursive: true })
45
+ try {
46
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
47
+ // should not reach this point
48
+ throw new Error("Expected an error")
49
+ } catch (e) {
50
+ expect((e as Error).message).toMatch(
51
+ 'projectPath already exists, received "/test/project.inlang"'
52
+ )
53
+ }
54
+ })
55
+
56
+ it("should create default defaultProjectSettings in projectPath", async () => {
57
+ const repo = await mockRepo()
58
+ const projectPath = "/test/project.inlang"
59
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
60
+ const json = await repo.nodeishFs.readFile(`${projectPath}/settings.json`, {
61
+ encoding: "utf-8",
62
+ })
63
+ const settings = JSON.parse(json)
64
+ expect(settings).toEqual(defaultProjectSettings)
65
+ })
66
+
67
+ it("should create different projectSettings in projectPath", async () => {
68
+ const repo = await mockRepo()
69
+ const projectPath = "/test/project.inlang"
70
+ const projectSettings = { ...defaultProjectSettings, languageTags: ["en", "de", "fr"] }
71
+ await createNewProject({ projectPath, repo, projectSettings })
72
+ const json = await repo.nodeishFs.readFile(`${projectPath}/settings.json`, {
73
+ encoding: "utf-8",
74
+ })
75
+ const settings = JSON.parse(json)
76
+ expect(settings).toEqual(projectSettings)
77
+ expect(settings).not.toEqual(defaultProjectSettings)
78
+ })
79
+
80
+ it("should load the project after creating it", async () => {
81
+ const repo = await mockRepo()
82
+ const projectPath = "/test/project.inlang"
83
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
84
+
85
+ const project = await loadProject({ projectPath, repo })
86
+ expect(project.errors().length).toBe(0)
87
+ })
88
+
89
+ it("should create messages inside the project directory", async () => {
90
+ const repo = await mockRepo()
91
+ const projectPath = "/test/project.inlang"
92
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
93
+ const project = await loadProject({ projectPath, repo })
94
+ expect(project.errors().length).toBe(0)
95
+
96
+ const testMessage = createMessage("test", { en: "test message" })
97
+ project.query.messages.create({ data: testMessage })
98
+ const messages = project.query.messages.getAll()
99
+ expect(messages.length).toBe(1)
100
+ expect(messages[0]).toEqual(testMessage)
101
+
102
+ await sleep(20)
103
+
104
+ const json = await repo.nodeishFs.readFile("/test/messages/en.json", { encoding: "utf-8" })
105
+ const jsonMessages = JSON.parse(json)
106
+ expect(jsonMessages["test"]).toEqual("test message")
107
+ })
108
+ })
@@ -0,0 +1,31 @@
1
+ import type { Repository } from "@lix-js/client"
2
+ import { ProjectSettings } from "@inlang/project-settings"
3
+ import { assertValidProjectPath, pathExists } from "./validateProjectPath.js"
4
+ import { defaultProjectSettings } from "./defaultProjectSettings.js"
5
+
6
+ /**
7
+ * Creates a new project in the given directory.
8
+ * The directory must be an absolute path, must not exist, and must end with {name}.inlang
9
+ * Uses defaultProjectSettings unless projectSettings are provided.
10
+ *
11
+ * @param projectPath - Absolute path to the [name].inlang directory
12
+ * @param repo - An instance of a lix repo as returned by `openRepository`
13
+ * @param projectSettings - Optional project settings to use for the new project.
14
+ */
15
+ export async function createNewProject(args: {
16
+ projectPath: string
17
+ repo: Repository
18
+ projectSettings: ProjectSettings
19
+ }): Promise<void> {
20
+ assertValidProjectPath(args.projectPath)
21
+
22
+ const nodeishFs = args.repo.nodeishFs
23
+ if (await pathExists(args.projectPath, nodeishFs)) {
24
+ throw new Error(`projectPath already exists, received "${args.projectPath}"`)
25
+ }
26
+ await nodeishFs.mkdir(args.projectPath, { recursive: true })
27
+
28
+ const settingsText = JSON.stringify(args.projectSettings ?? defaultProjectSettings, undefined, 2)
29
+
30
+ await nodeishFs.writeFile(`${args.projectPath}/settings.json`, settingsText)
31
+ }
@@ -1,5 +1,5 @@
1
1
  import { normalizePath, type NodeishFilesystem } from "@lix-js/fs"
2
- import { isAbsolutePath } from "./isAbsolutePath.js"
2
+ import { isAbsolutePath } from "./validateProjectPath.js"
3
3
 
4
4
  /**
5
5
  * Wraps the nodeish filesystem subset with a function that intercepts paths
@@ -0,0 +1,27 @@
1
+ import type { ProjectSettings } from "@inlang/project-settings"
2
+
3
+ /**
4
+ * Default project settings for createNewProject
5
+ * from paraglide-js/src/cli/commands/init/defaults.ts
6
+ */
7
+ export const defaultProjectSettings = {
8
+ $schema: "https://inlang.com/schema/project-settings",
9
+ sourceLanguageTag: "en",
10
+ languageTags: ["en"],
11
+ modules: [
12
+ // for instant gratification, we're adding common rules
13
+ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
14
+ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js",
15
+ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js",
16
+ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@latest/dist/index.js",
17
+
18
+ // default to the message format plugin because it supports all features
19
+ "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js",
20
+
21
+ // the m function matcher should be installed by default in case Sherlock (VS Code extension) is adopted
22
+ "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js",
23
+ ],
24
+ "plugin.inlang.messageFormat": {
25
+ pathPattern: "./messages/{languageTag}.json",
26
+ },
27
+ } satisfies ProjectSettings
package/src/index.ts CHANGED
@@ -12,6 +12,8 @@ export type {
12
12
  Subscribable,
13
13
  } from "./api.js"
14
14
  export { type ImportFunction, createImport } from "./resolve-modules/index.js"
15
+ export { createNewProject } from "./createNewProject.js"
16
+ export { defaultProjectSettings } from "./defaultProjectSettings.js"
15
17
  export { loadProject } from "./loadProject.js"
16
18
  export { listProjects } from "./listProjects.js"
17
19
  export { solidAdapter, type InlangProjectWithSolidAdapter } from "./adapter/solidAdapter.js"
@@ -24,6 +26,7 @@ export {
24
26
  PluginSaveMessagesError,
25
27
  } from "./errors.js"
26
28
 
29
+ export { normalizeMessage } from "./storage/helper.js"
27
30
  export * from "./messages/variant.js"
28
31
  export * from "./versionedInterfaces.js"
29
32
  export { InlangModule } from "@inlang/module"
@@ -11,7 +11,6 @@ import type {
11
11
  import type { ImportFunction } from "./resolve-modules/index.js"
12
12
  import type { InlangModule } from "@inlang/module"
13
13
  import {
14
- LoadProjectInvalidArgument,
15
14
  ProjectSettingsFileJSONSyntaxError,
16
15
  ProjectSettingsFileNotFoundError,
17
16
  ProjectSettingsInvalidError,
@@ -175,7 +174,7 @@ it("should throw if a project (path) does not have a name", async () => {
175
174
  _import,
176
175
  })
177
176
  )
178
- expect(project.error).toBeInstanceOf(LoadProjectInvalidArgument)
177
+ expect(project?.error?.message).toMatch('Expected a path ending in "{name}.inlang" but received ')
179
178
  })
180
179
 
181
180
  it("should throw if a project path does not end with .inlang", async () => {
@@ -195,7 +194,9 @@ it("should throw if a project path does not end with .inlang", async () => {
195
194
  _import,
196
195
  })
197
196
  )
198
- expect(project.error).toBeInstanceOf(LoadProjectInvalidArgument)
197
+ expect(project.error?.message).toMatch(
198
+ 'Expected a path ending in "{name}.inlang" but received '
199
+ )
199
200
  }
200
201
  })
201
202
 
@@ -210,7 +211,7 @@ describe("initialization", () => {
210
211
  _import,
211
212
  })
212
213
  )
213
- expect(result.error).toBeInstanceOf(LoadProjectInvalidArgument)
214
+ expect(result.error?.message).toBe('Expected an absolute path but received "relative/path".')
214
215
  expect(result.data).toBeUndefined()
215
216
  })
216
217
 
@@ -12,7 +12,6 @@ import {
12
12
  ProjectSettingsFileNotFoundError,
13
13
  ProjectSettingsInvalidError,
14
14
  PluginSaveMessagesError,
15
- LoadProjectInvalidArgument,
16
15
  PluginLoadMessagesError,
17
16
  } from "./errors.js"
18
17
  import { createRoot, createSignal, createEffect } from "./reactivity/solid.js"
@@ -23,7 +22,7 @@ import { tryCatch, type Result } from "@inlang/result"
23
22
  import { migrateIfOutdated } from "@inlang/project-settings/migration"
24
23
  import { createNodeishFsWithAbsolutePaths } from "./createNodeishFsWithAbsolutePaths.js"
25
24
  import { normalizePath, type NodeishFilesystem } from "@lix-js/fs"
26
- import { isAbsolutePath } from "./isAbsolutePath.js"
25
+ import { assertValidProjectPath } from "./validateProjectPath.js"
27
26
  import { maybeMigrateToDirectory } from "./migrations/migrateToDirectory.js"
28
27
 
29
28
  import { stringifyMessage as stringifyMessage } from "./storage/helper.js"
@@ -40,7 +39,8 @@ import { identifyProject } from "./telemetry/groupIdentify.js"
40
39
  import type { NodeishStats } from "@lix-js/fs"
41
40
 
42
41
  import _debug from "debug"
43
- const debug = _debug("loadProject")
42
+ const debug = _debug("sdk:loadProject")
43
+ const debugLock = _debug("sdk:lockfile")
44
44
 
45
45
  const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
46
46
 
@@ -93,17 +93,7 @@ export async function loadProject(args: {
93
93
  // won't even be loaded. do not throw anywhere else. otherwise, apps
94
94
  // can't handle errors gracefully.
95
95
 
96
- if (!isAbsolutePath(args.projectPath)) {
97
- throw new LoadProjectInvalidArgument(
98
- `Expected an absolute path but received "${args.projectPath}".`,
99
- { argument: "projectPath" }
100
- )
101
- } else if (/[^\\/]+\.inlang$/.test(projectPath) === false) {
102
- throw new LoadProjectInvalidArgument(
103
- `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`,
104
- { argument: "projectPath" }
105
- )
106
- }
96
+ assertValidProjectPath(projectPath)
107
97
 
108
98
  const nodeishFs = createNodeishFsWithAbsolutePaths({
109
99
  projectPath,
@@ -674,7 +664,7 @@ async function loadMessagesViaPlugin(
674
664
 
675
665
  // NOTE could use hash instead of the whole object JSON to save memory...
676
666
  if (messageState.messageLoadHash[loadedMessageClone.id] === importedEnecoded) {
677
- debug("skipping upsert!")
667
+ // debug("skipping upsert!")
678
668
  continue
679
669
  }
680
670
 
@@ -913,10 +903,10 @@ async function acquireFileLock(
913
903
  }
914
904
 
915
905
  try {
916
- debug(lockOrigin + " tries to acquire a lockfile Retry Nr.: " + tryCount)
906
+ debugLock(lockOrigin + " tries to acquire a lockfile Retry Nr.: " + tryCount)
917
907
  await fs.mkdir(lockDirPath)
918
908
  const stats = await fs.stat(lockDirPath)
919
- debug(lockOrigin + " acquired a lockfile Retry Nr.: " + tryCount)
909
+ debugLock(lockOrigin + " acquired a lockfile Retry Nr.: " + tryCount)
920
910
  return stats.mtimeMs
921
911
  } catch (error: any) {
922
912
  if (error.code !== "EEXIST") {
@@ -933,12 +923,14 @@ async function acquireFileLock(
933
923
  } catch (fstatError: any) {
934
924
  if (fstatError.code === "ENOENT") {
935
925
  // lock file seems to be gone :) - lets try again
936
- debug(lockOrigin + " tryCount++ lock file seems to be gone :) - lets try again " + tryCount)
926
+ debugLock(
927
+ lockOrigin + " tryCount++ lock file seems to be gone :) - lets try again " + tryCount
928
+ )
937
929
  return acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1)
938
930
  }
939
931
  throw fstatError
940
932
  }
941
- debug(
933
+ debugLock(
942
934
  lockOrigin +
943
935
  " tries to acquire a lockfile - lock currently in use... starting probe phase " +
944
936
  tryCount
@@ -951,7 +943,7 @@ async function acquireFileLock(
951
943
  probeCounts += 1
952
944
  let lockFileStats: undefined | NodeishStats = undefined
953
945
  try {
954
- debug(
946
+ debugLock(
955
947
  lockOrigin + " tries to acquire a lockfile - check if the lock is free now " + tryCount
956
948
  )
957
949
 
@@ -959,7 +951,7 @@ async function acquireFileLock(
959
951
  lockFileStats = await fs.stat(lockDirPath)
960
952
  } catch (fstatError: any) {
961
953
  if (fstatError.code === "ENOENT") {
962
- debug(
954
+ debugLock(
963
955
  lockOrigin +
964
956
  " tryCount++ in Promise - tries to acquire a lockfile - lock file seems to be free now - try to acquire " +
965
957
  tryCount
@@ -974,7 +966,7 @@ async function acquireFileLock(
974
966
  if (lockFileStats.mtimeMs === currentLockTime) {
975
967
  if (probeCounts >= nProbes) {
976
968
  // ok maximum lock time ran up (we waitetd nProbes * probeInterval) - we consider the lock to be stale
977
- debug(
969
+ debugLock(
978
970
  lockOrigin +
979
971
  " tries to acquire a lockfile - lock not free - but stale lets drop it" +
980
972
  tryCount
@@ -990,7 +982,7 @@ async function acquireFileLock(
990
982
  return reject(rmLockError)
991
983
  }
992
984
  try {
993
- debug(
985
+ debugLock(
994
986
  lockOrigin +
995
987
  " tryCount++ same locker - try to acquire again after removing stale lock " +
996
988
  tryCount
@@ -1006,7 +998,9 @@ async function acquireFileLock(
1006
998
  }
1007
999
  } else {
1008
1000
  try {
1009
- debug(lockOrigin + " tryCount++ different locker - try to acquire again " + tryCount)
1001
+ debugLock(
1002
+ lockOrigin + " tryCount++ different locker - try to acquire again " + tryCount
1003
+ )
1010
1004
  const lock = await acquireFileLock(fs, lockDirPath, lockOrigin, tryCount + 1)
1011
1005
  return resolve(lock)
1012
1006
  } catch (error) {
@@ -1025,7 +1019,7 @@ async function releaseLock(
1025
1019
  lockOrigin: string,
1026
1020
  lockTime: number
1027
1021
  ) {
1028
- debug(lockOrigin + " releasing the lock ")
1022
+ debugLock(lockOrigin + " releasing the lock ")
1029
1023
  try {
1030
1024
  const stats = await fs.stat(lockDirPath)
1031
1025
  if (stats.mtimeMs === lockTime) {
@@ -1033,13 +1027,13 @@ async function releaseLock(
1033
1027
  await fs.rmdir(lockDirPath)
1034
1028
  }
1035
1029
  } catch (statError: any) {
1036
- debug(lockOrigin + " couldn't release the lock")
1030
+ debugLock(lockOrigin + " couldn't release the lock")
1037
1031
  if (statError.code === "ENOENT") {
1038
1032
  // ok seeks like the log was released by someone else
1039
- debug(lockOrigin + " WARNING - the lock was released by a different process")
1033
+ debugLock(lockOrigin + " WARNING - the lock was released by a different process")
1040
1034
  return
1041
1035
  }
1042
- debug(statError)
1036
+ debugLock(statError)
1043
1037
  throw statError
1044
1038
  }
1045
1039
  }
@@ -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 { type Snapshot } from "@lix-js/fs"
6
5
  // eslint-disable-next-line no-restricted-imports -- test
7
6
  import { readFileSync } from "node:fs"
@@ -22,12 +21,10 @@ it("should generate a project id", async () => {
22
21
  })
23
22
 
24
23
  it("should return undefined if repoMeta contains error", async () => {
25
- const repoWithError = await openRepository("https://github.com/inlang/no-exist", {
26
- nodeishFs: createNodeishMemoryFs(),
27
- })
24
+ await repo.nodeishFs.rm("/.git", { recursive: true })
28
25
 
29
26
  const projectId = await generateProjectId({
30
- repo: repoWithError,
27
+ repo: repo,
31
28
  projectPath: "mocked_project_path",
32
29
  })
33
30
  expect(projectId).toBeUndefined()
@@ -22,27 +22,53 @@ export function getPathFromMessageId(id: string) {
22
22
  return path
23
23
  }
24
24
 
25
- export function stringifyMessage(message: Message) {
26
- // create a new object do specify key output order
25
+ /**
26
+ * Returns a copy of a message object with sorted variants and object keys.
27
+ * This produces a deterministic result when passed to stringify
28
+ * independent of the initialization order.
29
+ */
30
+ export function normalizeMessage(message: Message) {
31
+ // order keys in message
27
32
  const messageWithSortedKeys: any = {}
28
33
  for (const key of Object.keys(message).sort()) {
29
34
  messageWithSortedKeys[key] = (message as any)[key]
30
35
  }
31
36
 
32
- // lets order variants as well
33
- messageWithSortedKeys["variants"] = messageWithSortedKeys["variants"].sort(
34
- (variantA: Variant, variantB: Variant) => {
35
- // First, compare by language
37
+ // order variants
38
+ messageWithSortedKeys["variants"] = messageWithSortedKeys["variants"]
39
+ .sort((variantA: Variant, variantB: Variant) => {
40
+ // compare by language
36
41
  const languageComparison = variantA.languageTag.localeCompare(variantB.languageTag)
37
42
 
38
- // If languages are the same, compare by match
43
+ // if languages are the same, compare by match
39
44
  if (languageComparison === 0) {
40
45
  return variantA.match.join("-").localeCompare(variantB.match.join("-"))
41
46
  }
42
47
 
43
48
  return languageComparison
44
- }
45
- )
49
+ })
50
+ // order keys in each variant
51
+ .map((variant: Variant) => {
52
+ const variantWithSortedKeys: any = {}
53
+ for (const variantKey of Object.keys(variant).sort()) {
54
+ if (variantKey === "pattern") {
55
+ variantWithSortedKeys[variantKey] = (variant as any)["pattern"].map((token: any) => {
56
+ const tokenWithSortedKey: any = {}
57
+ for (const tokenKey of Object.keys(token).sort()) {
58
+ tokenWithSortedKey[tokenKey] = token[tokenKey]
59
+ }
60
+ return tokenWithSortedKey
61
+ })
62
+ } else {
63
+ variantWithSortedKeys[variantKey] = (variant as any)[variantKey]
64
+ }
65
+ }
66
+ return variantWithSortedKeys
67
+ })
46
68
 
47
- return JSON.stringify(messageWithSortedKeys, undefined, 4)
69
+ return messageWithSortedKeys as Message
70
+ }
71
+
72
+ export function stringifyMessage(message: Message) {
73
+ return JSON.stringify(normalizeMessage(message), undefined, 4)
48
74
  }