@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,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 @@
|
|
|
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.
|
|
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.
|
|
39
|
-
"@inlang/
|
|
40
|
-
"@inlang/
|
|
41
|
-
"@inlang/project-settings": "2.4.
|
|
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.
|
|
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
|
+
}
|
|
@@ -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"
|
package/src/loadProject.test.ts
CHANGED
|
@@ -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
|
|
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).
|
|
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).
|
|
214
|
+
expect(result.error?.message).toBe('Expected an absolute path but received "relative/path".')
|
|
214
215
|
expect(result.data).toBeUndefined()
|
|
215
216
|
})
|
|
216
217
|
|
package/src/loadProject.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1033
|
+
debugLock(lockOrigin + " WARNING - the lock was released by a different process")
|
|
1040
1034
|
return
|
|
1041
1035
|
}
|
|
1042
|
-
|
|
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 {
|
|
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
|
-
|
|
26
|
-
nodeishFs: createNodeishMemoryFs(),
|
|
27
|
-
})
|
|
24
|
+
await repo.nodeishFs.rm("/.git", { recursive: true })
|
|
28
25
|
|
|
29
26
|
const projectId = await generateProjectId({
|
|
30
|
-
repo:
|
|
27
|
+
repo: repo,
|
|
31
28
|
projectPath: "mocked_project_path",
|
|
32
29
|
})
|
|
33
30
|
expect(projectId).toBeUndefined()
|
package/src/storage/helper.ts
CHANGED
|
@@ -22,27 +22,53 @@ export function getPathFromMessageId(id: string) {
|
|
|
22
22
|
return path
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Returns a copy of a message object with sorted variants and object keys.
|
|
27
|
+
* This produces a deterministic result when passed to stringify
|
|
28
|
+
* independent of the initialization order.
|
|
29
|
+
*/
|
|
30
|
+
export function normalizeMessage(message: Message) {
|
|
31
|
+
// order keys in message
|
|
27
32
|
const messageWithSortedKeys: any = {}
|
|
28
33
|
for (const key of Object.keys(message).sort()) {
|
|
29
34
|
messageWithSortedKeys[key] = (message as any)[key]
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
//
|
|
33
|
-
messageWithSortedKeys["variants"] = messageWithSortedKeys["variants"]
|
|
34
|
-
(variantA: Variant, variantB: Variant) => {
|
|
35
|
-
//
|
|
37
|
+
// order variants
|
|
38
|
+
messageWithSortedKeys["variants"] = messageWithSortedKeys["variants"]
|
|
39
|
+
.sort((variantA: Variant, variantB: Variant) => {
|
|
40
|
+
// compare by language
|
|
36
41
|
const languageComparison = variantA.languageTag.localeCompare(variantB.languageTag)
|
|
37
42
|
|
|
38
|
-
//
|
|
43
|
+
// if languages are the same, compare by match
|
|
39
44
|
if (languageComparison === 0) {
|
|
40
45
|
return variantA.match.join("-").localeCompare(variantB.match.join("-"))
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
return languageComparison
|
|
44
|
-
}
|
|
45
|
-
|
|
49
|
+
})
|
|
50
|
+
// order keys in each variant
|
|
51
|
+
.map((variant: Variant) => {
|
|
52
|
+
const variantWithSortedKeys: any = {}
|
|
53
|
+
for (const variantKey of Object.keys(variant).sort()) {
|
|
54
|
+
if (variantKey === "pattern") {
|
|
55
|
+
variantWithSortedKeys[variantKey] = (variant as any)["pattern"].map((token: any) => {
|
|
56
|
+
const tokenWithSortedKey: any = {}
|
|
57
|
+
for (const tokenKey of Object.keys(token).sort()) {
|
|
58
|
+
tokenWithSortedKey[tokenKey] = token[tokenKey]
|
|
59
|
+
}
|
|
60
|
+
return tokenWithSortedKey
|
|
61
|
+
})
|
|
62
|
+
} else {
|
|
63
|
+
variantWithSortedKeys[variantKey] = (variant as any)[variantKey]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return variantWithSortedKeys
|
|
67
|
+
})
|
|
46
68
|
|
|
47
|
-
return
|
|
69
|
+
return messageWithSortedKeys as Message
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function stringifyMessage(message: Message) {
|
|
73
|
+
return JSON.stringify(normalizeMessage(message), undefined, 4)
|
|
48
74
|
}
|