@inlang/sdk 0.19.0 → 0.21.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/README.md +1 -1
- package/dist/createMessageLintReportsQuery.d.ts +2 -3
- package/dist/createMessageLintReportsQuery.d.ts.map +1 -1
- package/dist/createMessageLintReportsQuery.js +42 -21
- package/dist/generateProjectId.d.ts +3 -0
- package/dist/generateProjectId.d.ts.map +1 -0
- package/dist/generateProjectId.js +11 -0
- package/dist/generateProjectId.test.d.ts +2 -0
- package/dist/generateProjectId.test.d.ts.map +1 -0
- package/dist/generateProjectId.test.js +18 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/listProjects.d.ts +5 -0
- package/dist/listProjects.d.ts.map +1 -0
- package/dist/listProjects.js +31 -0
- package/dist/listProjects.test.d.ts +2 -0
- package/dist/listProjects.test.d.ts.map +1 -0
- package/dist/listProjects.test.js +56 -0
- package/dist/loadProject.d.ts +2 -0
- package/dist/loadProject.d.ts.map +1 -1
- package/dist/loadProject.js +36 -6
- package/dist/loadProject.test.js +49 -0
- package/package.json +11 -10
- package/src/createMessageLintReportsQuery.ts +57 -28
- package/src/generateProjectId.test.ts +22 -0
- package/src/generateProjectId.ts +14 -0
- package/src/index.ts +1 -0
- package/src/listProjects.test.ts +69 -0
- package/src/listProjects.ts +39 -0
- package/src/loadProject.test.ts +66 -1
- package/src/loadProject.ts +42 -7
package/README.md
CHANGED
|
@@ -13,4 +13,4 @@ Developer-first localization infrastructure that is built on git. Your git repos
|
|
|
13
13
|
|
|
14
14
|
# @inlang/sdk
|
|
15
15
|
|
|
16
|
-
The core module bundles "sdk" modules that depend on each other and is everything one needs to build [plugins](https://inlang.com/documentation/plugin) or entire [apps](https://inlang.com/documentation/
|
|
16
|
+
The core module bundles "sdk" modules that depend on each other and is everything one needs to build [plugins](https://inlang.com/documentation/plugin) or entire [apps](https://inlang.com/documentation/build-app) on inlang.
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type { InlangProject, InstalledMessageLintRule } from "./api.js";
|
|
1
|
+
import type { InlangProject, InstalledMessageLintRule, MessageQueryApi } from "./api.js";
|
|
2
2
|
import type { ProjectSettings } from "@inlang/project-settings";
|
|
3
3
|
import type { resolveModules } from "./resolve-modules/index.js";
|
|
4
|
-
import type { Message } from "./versionedInterfaces.js";
|
|
5
4
|
/**
|
|
6
5
|
* Creates a reactive query API for messages.
|
|
7
6
|
*/
|
|
8
|
-
export declare function createMessageLintReportsQuery(
|
|
7
|
+
export declare function createMessageLintReportsQuery(messagesQuery: MessageQueryApi, settings: () => ProjectSettings, installedMessageLintRules: () => Array<InstalledMessageLintRule>, resolvedModules: () => Awaited<ReturnType<typeof resolveModules>> | undefined, hasWatcher: boolean): InlangProject["query"]["messageLintReports"];
|
|
9
8
|
//# sourceMappingURL=createMessageLintReportsQuery.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createMessageLintReportsQuery.d.ts","sourceRoot":"","sources":["../src/createMessageLintReportsQuery.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"createMessageLintReportsQuery.d.ts","sourceRoot":"","sources":["../src/createMessageLintReportsQuery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,aAAa,EACb,wBAAwB,EAExB,eAAe,EACf,MAAM,UAAU,CAAA;AACjB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAOhE;;GAEG;AACH,wBAAgB,6BAA6B,CAC5C,aAAa,EAAE,eAAe,EAC9B,QAAQ,EAAE,MAAM,eAAe,EAC/B,yBAAyB,EAAE,MAAM,KAAK,CAAC,wBAAwB,CAAC,EAChE,eAAe,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC,GAAG,SAAS,EAC7E,UAAU,EAAE,OAAO,GACjB,aAAa,CAAC,OAAO,CAAC,CAAC,oBAAoB,CAAC,CA2E9C"}
|
|
@@ -1,33 +1,54 @@
|
|
|
1
|
-
import { createEffect } from "./reactivity/solid.js";
|
|
2
1
|
import { createSubscribable } from "./loadProject.js";
|
|
3
2
|
import { lintSingleMessage } from "./lint/index.js";
|
|
4
3
|
import { ReactiveMap } from "./reactivity/map.js";
|
|
4
|
+
import { debounce } from "throttle-debounce";
|
|
5
|
+
import { createEffect } from "./reactivity/solid.js";
|
|
5
6
|
/**
|
|
6
7
|
* Creates a reactive query API for messages.
|
|
7
8
|
*/
|
|
8
|
-
export function createMessageLintReportsQuery(
|
|
9
|
+
export function createMessageLintReportsQuery(messagesQuery, settings, installedMessageLintRules, resolvedModules, hasWatcher) {
|
|
9
10
|
// @ts-expect-error
|
|
10
11
|
const index = new ReactiveMap();
|
|
12
|
+
const modules = resolvedModules();
|
|
13
|
+
const rulesArray = modules?.messageLintRules;
|
|
14
|
+
const messageLintRuleLevels = Object.fromEntries(installedMessageLintRules().map((rule) => [rule.id, rule.level]));
|
|
15
|
+
const settingsObject = () => {
|
|
16
|
+
return {
|
|
17
|
+
...settings(),
|
|
18
|
+
messageLintRuleLevels,
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const messages = messagesQuery.getAll();
|
|
11
22
|
createEffect(() => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
if (rulesArray) {
|
|
24
|
+
for (const messageId of messagesQuery.includedMessageIds()) {
|
|
25
|
+
createEffect(() => {
|
|
26
|
+
const message = messagesQuery.get({ where: { id: messageId } });
|
|
27
|
+
if (hasWatcher) {
|
|
28
|
+
lintSingleMessage({
|
|
29
|
+
rules: rulesArray,
|
|
30
|
+
settings: settingsObject(),
|
|
31
|
+
messages: messages,
|
|
32
|
+
message: message,
|
|
33
|
+
}).then((report) => {
|
|
34
|
+
if (report.errors.length === 0 && index.get(messageId) !== report.data) {
|
|
35
|
+
index.set(messageId, report.data);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
debounce(500, (message) => {
|
|
41
|
+
lintSingleMessage({
|
|
42
|
+
rules: rulesArray,
|
|
43
|
+
settings: settingsObject(),
|
|
44
|
+
messages: messages,
|
|
45
|
+
message: message,
|
|
46
|
+
}).then((report) => {
|
|
47
|
+
if (report.errors.length === 0 && index.get(messageId) !== report.data) {
|
|
48
|
+
index.set(messageId, report.data);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}, { atBegin: false })(message);
|
|
31
52
|
}
|
|
32
53
|
});
|
|
33
54
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generateProjectId.d.ts","sourceRoot":"","sources":["../src/generateProjectId.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAGhD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,+BAU5E"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { hash } from "@lix-js/client";
|
|
2
|
+
export async function generateProjectId(repo, projectPath) {
|
|
3
|
+
if (!repo || !projectPath) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
const repoMeta = await repo.getMeta();
|
|
7
|
+
if (repoMeta && !("error" in repoMeta)) {
|
|
8
|
+
return hash(`${repoMeta.id + projectPath}`);
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generateProjectId.test.d.ts","sourceRoot":"","sources":["../src/generateProjectId.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { generateProjectId } from "./generateProjectId.js";
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { openRepository } from "@lix-js/client/src/openRepository.ts";
|
|
4
|
+
import { mockRepo, createNodeishMemoryFs } from "@lix-js/client";
|
|
5
|
+
describe("generateProjectId", async () => {
|
|
6
|
+
const repo = await mockRepo();
|
|
7
|
+
it("should generate a project id", async () => {
|
|
8
|
+
const projectId = await generateProjectId(repo, "mocked_project_path");
|
|
9
|
+
expect(projectId).toBe("0c83325bf9068eb01091c522d4b8e3765aff42e36fc781c041b44439bbe3e734");
|
|
10
|
+
});
|
|
11
|
+
it("should return undefined if repoMeta contains error", async () => {
|
|
12
|
+
const errorRepo = await openRepository("https://github.com/inlang/no-exist", {
|
|
13
|
+
nodeishFs: createNodeishMemoryFs(),
|
|
14
|
+
});
|
|
15
|
+
const projectId = await generateProjectId(errorRepo, "mocked_project_path");
|
|
16
|
+
expect(projectId).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
export type { InlangProject, InstalledMessageLintRule, InstalledPlugin, MessageQueryApi, Subscribable, } from "./api.js";
|
|
7
7
|
export { type ImportFunction, createImport } from "./resolve-modules/index.js";
|
|
8
8
|
export { loadProject } from "./loadProject.js";
|
|
9
|
+
export { listProjects } from "./listProjects.js";
|
|
9
10
|
export { solidAdapter, type InlangProjectWithSolidAdapter } from "./adapter/solidAdapter.js";
|
|
10
11
|
export { createMessagesQuery } from "./createMessagesQuery.js";
|
|
11
12
|
export { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginLoadMessagesError, PluginSaveMessagesError, } from "./errors.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EACX,aAAa,EACb,wBAAwB,EACxB,eAAe,EACf,eAAe,EACf,YAAY,GACZ,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,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,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"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { createImport } from "./resolve-modules/index.js";
|
|
7
7
|
export { loadProject } from "./loadProject.js";
|
|
8
|
+
export { listProjects } from "./listProjects.js";
|
|
8
9
|
export { solidAdapter } from "./adapter/solidAdapter.js";
|
|
9
10
|
export { createMessagesQuery } from "./createMessagesQuery.js";
|
|
10
11
|
export { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginLoadMessagesError, PluginSaveMessagesError, } from "./errors.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listProjects.d.ts","sourceRoot":"","sources":["../src/listProjects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD,eAAO,MAAM,YAAY,cACb,iBAAiB,QACtB,MAAM,KACV,QAAQ,MAAM;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CAiCxC,CAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const listProjects = async (nodeishFs, from) => {
|
|
2
|
+
// !TODO: Remove this limit once we introduce caching
|
|
3
|
+
const recursionLimit = 5;
|
|
4
|
+
const projects = [];
|
|
5
|
+
async function searchDir(path, depth) {
|
|
6
|
+
if (depth > recursionLimit) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const files = await nodeishFs.readdir(path);
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
const filePath = `${path}/${file}`;
|
|
12
|
+
const stats = await nodeishFs.stat(filePath);
|
|
13
|
+
if (stats.isDirectory()) {
|
|
14
|
+
if (file === "node_modules")
|
|
15
|
+
continue;
|
|
16
|
+
if (file.endsWith(".inlang")) {
|
|
17
|
+
projects.push({ projectPath: filePath });
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
await searchDir(filePath, depth + 1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
await searchDir(from, 0);
|
|
26
|
+
// remove double slashes
|
|
27
|
+
for (const project of projects) {
|
|
28
|
+
project.projectPath = project.projectPath.replace(/\/\//g, "/");
|
|
29
|
+
}
|
|
30
|
+
return projects;
|
|
31
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listProjects.test.d.ts","sourceRoot":"","sources":["../src/listProjects.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { assert, describe, it } from "vitest";
|
|
2
|
+
import { listProjects } from "./listProjects.js";
|
|
3
|
+
import { createNodeishMemoryFs } from "@lix-js/fs";
|
|
4
|
+
const settings = {
|
|
5
|
+
sourceLanguageTag: "en",
|
|
6
|
+
languageTags: ["en"],
|
|
7
|
+
modules: ["plugin.js", "lintRule.js"],
|
|
8
|
+
messageLintRuleLevels: {
|
|
9
|
+
"messageLintRule.project.missingTranslation": "error",
|
|
10
|
+
},
|
|
11
|
+
"plugin.project.i18next": {
|
|
12
|
+
pathPattern: "./examples/example01/{languageTag}.json",
|
|
13
|
+
variableReferencePattern: ["{", "}"],
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
describe("listProjects", () => {
|
|
17
|
+
it("should find all projects a given path", async () => {
|
|
18
|
+
const fs = createNodeishMemoryFs();
|
|
19
|
+
await fs.mkdir("/user/dir1/project.inlang", { recursive: true });
|
|
20
|
+
await fs.writeFile("/user/dir1/project.inlang/settings.json", JSON.stringify(settings));
|
|
21
|
+
await fs.mkdir("/user/dir2/project.inlang", { recursive: true });
|
|
22
|
+
await fs.writeFile("/user/dir2/project.inlang/settings.json", JSON.stringify(settings));
|
|
23
|
+
await listProjects(fs, "/user").then((projects) => {
|
|
24
|
+
assert(projects.length === 2);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it("should return objects inside of an array with the projectPath", async () => {
|
|
28
|
+
const fs = createNodeishMemoryFs();
|
|
29
|
+
await fs.mkdir("/user/dir1/project.inlang", { recursive: true });
|
|
30
|
+
await fs.writeFile("/user/dir1/project.inlang/settings.json", JSON.stringify(settings));
|
|
31
|
+
await fs.mkdir("/user/dir2/project.inlang", { recursive: true });
|
|
32
|
+
await fs.writeFile("/user/dir2/project.inlang/settings.json", JSON.stringify(settings));
|
|
33
|
+
await listProjects(fs, "/user").then((projects) => {
|
|
34
|
+
assert.isTrue(typeof projects[0] === "object");
|
|
35
|
+
assert.isTrue(typeof projects[0]?.projectPath === "string");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it("should limit the recursion depth to 5", async () => {
|
|
39
|
+
const fs = createNodeishMemoryFs();
|
|
40
|
+
await fs.mkdir("/user/dir1/dir2/dir3/dir4/dir5/dir6/project.inlang", { recursive: true });
|
|
41
|
+
await fs.writeFile("/user/dir1/dir2/dir3/dir4/dir5/dir6/project.inlang/settings.json", JSON.stringify(settings));
|
|
42
|
+
await listProjects(fs, "/user").then((projects) => {
|
|
43
|
+
assert(projects.length === 0);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
it("should also find files inside of a dir that ends with *.inlang", async () => {
|
|
47
|
+
const fs = createNodeishMemoryFs();
|
|
48
|
+
await fs.mkdir("/user/dir1/go.inlang", { recursive: true });
|
|
49
|
+
await fs.writeFile("/user/dir1/go.inlang/settings.json", JSON.stringify(settings));
|
|
50
|
+
await fs.mkdir("/user/dir2/flutter.inlang", { recursive: true });
|
|
51
|
+
await fs.writeFile("/user/dir2/flutter.inlang/settings.json", JSON.stringify(settings));
|
|
52
|
+
await listProjects(fs, "/user").then((projects) => {
|
|
53
|
+
assert(projects.length === 2);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
package/dist/loadProject.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { InlangProject, Subscribable } from "./api.js";
|
|
2
2
|
import { type ImportFunction } from "./resolve-modules/index.js";
|
|
3
3
|
import { type NodeishFilesystem } from "@lix-js/fs";
|
|
4
|
+
import type { Repository } from "@lix-js/client";
|
|
4
5
|
/**
|
|
5
6
|
* Creates an inlang instance.
|
|
6
7
|
*
|
|
@@ -13,6 +14,7 @@ import { type NodeishFilesystem } from "@lix-js/fs";
|
|
|
13
14
|
*/
|
|
14
15
|
export declare const loadProject: (args: {
|
|
15
16
|
projectPath: string;
|
|
17
|
+
repo?: Repository | undefined;
|
|
16
18
|
nodeishFs: NodeishFilesystem;
|
|
17
19
|
_import?: ImportFunction | undefined;
|
|
18
20
|
_capture?: ((id: string, props: Record<string, unknown>) => void) | undefined;
|
|
@@ -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;AAkBhF,OAAO,EAAiB,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"loadProject.d.ts","sourceRoot":"","sources":["../src/loadProject.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,aAAa,EAGb,YAAY,EACZ,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,cAAc,EAAkB,MAAM,4BAA4B,CAAA;AAkBhF,OAAO,EAAiB,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAIlE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAKhD;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW;iBACV,MAAM;;eAER,iBAAiB;;qBAEZ,MAAM,SAAS,OAAO,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI;MAC5D,QAAQ,aAAa,CAkQxB,CAAA;AAwHD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAQtE"}
|
package/dist/loadProject.js
CHANGED
|
@@ -13,6 +13,7 @@ import { normalizePath } from "@lix-js/fs";
|
|
|
13
13
|
import { isAbsolutePath } from "./isAbsolutePath.js";
|
|
14
14
|
import { createNodeishFsWithWatcher } from "./createNodeishFsWithWatcher.js";
|
|
15
15
|
import { maybeMigrateToDirectory } from "./migrations/migrateToDirectory.js";
|
|
16
|
+
import { generateProjectId } from "./generateProjectId.js";
|
|
16
17
|
const settingsCompiler = TypeCompiler.Compile(ProjectSettings);
|
|
17
18
|
/**
|
|
18
19
|
* Creates an inlang instance.
|
|
@@ -39,21 +40,48 @@ export const loadProject = async (args) => {
|
|
|
39
40
|
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" });
|
|
40
41
|
}
|
|
41
42
|
// -- load project ------------------------------------------------------
|
|
43
|
+
let idError;
|
|
42
44
|
return await createRoot(async () => {
|
|
43
45
|
const [initialized, markInitAsComplete, markInitAsFailed] = createAwaitable();
|
|
44
46
|
const nodeishFs = createNodeishFsWithAbsolutePaths({
|
|
45
47
|
projectPath,
|
|
46
48
|
nodeishFs: args.nodeishFs,
|
|
47
49
|
});
|
|
50
|
+
let projectId;
|
|
51
|
+
try {
|
|
52
|
+
projectId = await nodeishFs.readFile(projectPath + "/project_id", {
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
if (error.code === "ENOENT") {
|
|
59
|
+
if (args.repo) {
|
|
60
|
+
projectId = await generateProjectId(args.repo, projectPath);
|
|
61
|
+
if (projectId) {
|
|
62
|
+
await nodeishFs.writeFile(projectPath + "/project_id", projectId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
idError = error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
48
70
|
// -- settings ------------------------------------------------------------
|
|
49
71
|
const [settings, _setSettings] = createSignal();
|
|
50
72
|
createEffect(() => {
|
|
73
|
+
// TODO:
|
|
74
|
+
// if (projectId) {
|
|
75
|
+
// telemetryBrowser.group("project", projectId, {
|
|
76
|
+
// name: projectId,
|
|
77
|
+
// })
|
|
78
|
+
// }
|
|
51
79
|
loadSettings({ settingsFilePath: projectPath + "/settings.json", nodeishFs })
|
|
52
80
|
.then((settings) => {
|
|
53
81
|
setSettings(settings);
|
|
54
82
|
// rename settings to get a convenient access to the data in Posthog
|
|
55
83
|
const project_settings = settings;
|
|
56
|
-
args._capture?.("SDK used settings", { project_settings });
|
|
84
|
+
args._capture?.("SDK used settings", { project_settings, group: projectId });
|
|
57
85
|
})
|
|
58
86
|
.catch((err) => {
|
|
59
87
|
markInitAsFailed(err);
|
|
@@ -90,11 +118,10 @@ export const loadProject = async (args) => {
|
|
|
90
118
|
// -- messages ----------------------------------------------------------
|
|
91
119
|
let settingsValue;
|
|
92
120
|
createEffect(() => (settingsValue = settings())); // workaround to not run effects twice (e.g. settings change + modules change) (I'm sure there exists a solid way of doing this, but I haven't found it yet)
|
|
121
|
+
// please don't use this as source of truth, use the query instead
|
|
122
|
+
// needed for granular linting
|
|
93
123
|
const [messages, setMessages] = createSignal();
|
|
94
124
|
createEffect(() => {
|
|
95
|
-
const conf = settings();
|
|
96
|
-
if (!conf)
|
|
97
|
-
return;
|
|
98
125
|
const _resolvedModules = resolvedModules();
|
|
99
126
|
if (!_resolvedModules)
|
|
100
127
|
return;
|
|
@@ -148,8 +175,10 @@ export const loadProject = async (args) => {
|
|
|
148
175
|
};
|
|
149
176
|
// -- app ---------------------------------------------------------------
|
|
150
177
|
const initializeError = await initialized.catch((error) => error);
|
|
178
|
+
const abortController = new AbortController();
|
|
179
|
+
const hasWatcher = nodeishFs.watch("/", { signal: abortController.signal }) !== undefined;
|
|
151
180
|
const messagesQuery = createMessagesQuery(() => messages() || []);
|
|
152
|
-
const lintReportsQuery = createMessageLintReportsQuery(
|
|
181
|
+
const lintReportsQuery = createMessageLintReportsQuery(messagesQuery, settings, installedMessageLintRules, resolvedModules, hasWatcher);
|
|
153
182
|
const debouncedSave = skipFirst(debounce(500, async (newMessages) => {
|
|
154
183
|
try {
|
|
155
184
|
if (JSON.stringify(newMessages) !== JSON.stringify(messages())) {
|
|
@@ -167,7 +196,7 @@ export const loadProject = async (args) => {
|
|
|
167
196
|
const abortController = new AbortController();
|
|
168
197
|
if (newMessages.length !== 0 &&
|
|
169
198
|
JSON.stringify(newMessages) !== JSON.stringify(messages()) &&
|
|
170
|
-
nodeishFs.watch("/", { signal: abortController.signal })
|
|
199
|
+
nodeishFs.watch("/", { signal: abortController.signal }) !== undefined) {
|
|
171
200
|
setMessages(newMessages);
|
|
172
201
|
}
|
|
173
202
|
}, { atBegin: false }));
|
|
@@ -181,6 +210,7 @@ export const loadProject = async (args) => {
|
|
|
181
210
|
},
|
|
182
211
|
errors: createSubscribable(() => [
|
|
183
212
|
...(initializeError ? [initializeError] : []),
|
|
213
|
+
...(idError ? [idError] : []),
|
|
184
214
|
...(resolvedModules() ? resolvedModules().errors : []),
|
|
185
215
|
// have a query error exposed
|
|
186
216
|
//...(lintErrors() ?? []),
|
package/dist/loadProject.test.js
CHANGED
|
@@ -5,6 +5,7 @@ import { LoadProjectInvalidArgument, ProjectSettingsFileJSONSyntaxError, Project
|
|
|
5
5
|
import { createNodeishMemoryFs, normalizePath } from "@lix-js/fs";
|
|
6
6
|
import { createMessage } from "./test-utilities/createMessage.js";
|
|
7
7
|
import { tryCatch } from "@inlang/result";
|
|
8
|
+
import { mockRepo } from "@lix-js/client";
|
|
8
9
|
// ------------------------------------------------------------------------------------------------
|
|
9
10
|
const getValue = (subscribable) => {
|
|
10
11
|
let value;
|
|
@@ -122,6 +123,54 @@ describe("initialization", () => {
|
|
|
122
123
|
expect(result.error).toBeInstanceOf(LoadProjectInvalidArgument);
|
|
123
124
|
expect(result.data).toBeUndefined();
|
|
124
125
|
});
|
|
126
|
+
it("should generate projectId on missing projectid", async () => {
|
|
127
|
+
const repo = await mockRepo();
|
|
128
|
+
const existing = await repo.nodeishFs
|
|
129
|
+
.readFile("/project.inlang/project_id", {
|
|
130
|
+
encoding: "utf-8",
|
|
131
|
+
})
|
|
132
|
+
.catch((error) => {
|
|
133
|
+
return { error };
|
|
134
|
+
});
|
|
135
|
+
// @ts-ignore
|
|
136
|
+
expect(existing.error.code).toBe("ENOENT");
|
|
137
|
+
const result = await tryCatch(() => loadProject({
|
|
138
|
+
projectPath: "/project.inlang",
|
|
139
|
+
nodeishFs: repo.nodeishFs,
|
|
140
|
+
repo,
|
|
141
|
+
_import,
|
|
142
|
+
}));
|
|
143
|
+
const newId = await repo.nodeishFs
|
|
144
|
+
.readFile("/project.inlang/project_id", {
|
|
145
|
+
encoding: "utf-8",
|
|
146
|
+
})
|
|
147
|
+
.catch((error) => {
|
|
148
|
+
return { error };
|
|
149
|
+
});
|
|
150
|
+
expect(newId).toBe("7cd6c2b7cf12febf99496408917123fdfe158b6bc442914f5fb42aa74346bd50");
|
|
151
|
+
expect(result.error).toBeUndefined();
|
|
152
|
+
expect(result.data).toBeDefined();
|
|
153
|
+
});
|
|
154
|
+
it("should reuse projectId on existing projectid", async () => {
|
|
155
|
+
const repo = await mockRepo();
|
|
156
|
+
repo.nodeishFs.writeFile("/project.inlang/project_id", "testId");
|
|
157
|
+
const result = await tryCatch(() => loadProject({
|
|
158
|
+
projectPath: "/project.inlang",
|
|
159
|
+
nodeishFs: repo.nodeishFs,
|
|
160
|
+
repo,
|
|
161
|
+
_import,
|
|
162
|
+
}));
|
|
163
|
+
const newId = await repo.nodeishFs
|
|
164
|
+
.readFile("/project.inlang/project_id", {
|
|
165
|
+
encoding: "utf-8",
|
|
166
|
+
})
|
|
167
|
+
.catch((error) => {
|
|
168
|
+
return { error };
|
|
169
|
+
});
|
|
170
|
+
expect(newId).toBe("testId");
|
|
171
|
+
expect(result.error).toBeUndefined();
|
|
172
|
+
expect(result.data).toBeDefined();
|
|
173
|
+
});
|
|
125
174
|
it("should resolve from a windows path", async () => {
|
|
126
175
|
const fs = createNodeishMemoryFs();
|
|
127
176
|
fs.mkdir("C:\\Users\\user\\project.inlang", { recursive: true });
|
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.21.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -21,20 +21,21 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@sinclair/typebox": "^0.31.17",
|
|
24
|
+
"dedent": "1.5.1",
|
|
24
25
|
"deepmerge-ts": "^5.1.0",
|
|
25
26
|
"solid-js": "1.6.12",
|
|
26
|
-
"throttle-debounce": "5.0.0",
|
|
27
|
-
"dedent": "1.5.1",
|
|
27
|
+
"throttle-debounce": "^5.0.0",
|
|
28
28
|
"@inlang/json-types": "1.1.0",
|
|
29
|
+
"@inlang/language-tag": "1.3.0",
|
|
29
30
|
"@inlang/message-lint-rule": "1.4.0",
|
|
30
|
-
"@inlang/translatable": "1.2.0",
|
|
31
|
-
"@inlang/module": "1.2.0",
|
|
32
|
-
"@inlang/language-tag": "1.2.0",
|
|
33
31
|
"@inlang/message": "2.0.0",
|
|
34
|
-
"@inlang/
|
|
35
|
-
"@inlang/
|
|
32
|
+
"@inlang/module": "1.2.1",
|
|
33
|
+
"@inlang/plugin": "2.4.1",
|
|
36
34
|
"@inlang/project-settings": "2.2.0",
|
|
37
|
-
"@
|
|
35
|
+
"@inlang/translatable": "1.2.0",
|
|
36
|
+
"@lix-js/fs": "0.5.0",
|
|
37
|
+
"@lix-js/client": "0.4.0",
|
|
38
|
+
"@inlang/result": "1.1.0"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@types/throttle-debounce": "5.0.0",
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"tsd": "^0.25.0",
|
|
45
46
|
"typescript": "5.2.2",
|
|
46
47
|
"vitest": "^0.33.0",
|
|
47
|
-
"@lix-js/fs": "0.
|
|
48
|
+
"@lix-js/fs": "0.5.0"
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
|
50
51
|
"build": "tsc --build",
|
|
@@ -1,50 +1,79 @@
|
|
|
1
|
-
import { createEffect } from "./reactivity/solid.js"
|
|
2
1
|
import { createSubscribable } from "./loadProject.js"
|
|
3
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
InlangProject,
|
|
4
|
+
InstalledMessageLintRule,
|
|
5
|
+
MessageLintReportsQueryApi,
|
|
6
|
+
MessageQueryApi,
|
|
7
|
+
} from "./api.js"
|
|
4
8
|
import type { ProjectSettings } from "@inlang/project-settings"
|
|
5
9
|
import type { resolveModules } from "./resolve-modules/index.js"
|
|
6
10
|
import type { MessageLintReport, Message } from "./versionedInterfaces.js"
|
|
7
11
|
import { lintSingleMessage } from "./lint/index.js"
|
|
8
12
|
import { ReactiveMap } from "./reactivity/map.js"
|
|
13
|
+
import { debounce } from "throttle-debounce"
|
|
14
|
+
import { createEffect } from "./reactivity/solid.js"
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* Creates a reactive query API for messages.
|
|
12
18
|
*/
|
|
13
19
|
export function createMessageLintReportsQuery(
|
|
14
|
-
|
|
20
|
+
messagesQuery: MessageQueryApi,
|
|
15
21
|
settings: () => ProjectSettings,
|
|
16
22
|
installedMessageLintRules: () => Array<InstalledMessageLintRule>,
|
|
17
|
-
resolvedModules: () => Awaited<ReturnType<typeof resolveModules>> | undefined
|
|
23
|
+
resolvedModules: () => Awaited<ReturnType<typeof resolveModules>> | undefined,
|
|
24
|
+
hasWatcher: boolean
|
|
18
25
|
): InlangProject["query"]["messageLintReports"] {
|
|
19
26
|
// @ts-expect-error
|
|
20
27
|
const index = new ReactiveMap<MessageLintReport["messageId"], MessageLintReport[]>()
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
const modules = resolvedModules()
|
|
24
|
-
const _messages = messages()
|
|
25
|
-
const _settings = settings()
|
|
29
|
+
const modules = resolvedModules()
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
const rulesArray = modules?.messageLintRules
|
|
32
|
+
const messageLintRuleLevels = Object.fromEntries(
|
|
33
|
+
installedMessageLintRules().map((rule) => [rule.id, rule.level])
|
|
34
|
+
)
|
|
35
|
+
const settingsObject = () => {
|
|
36
|
+
return {
|
|
37
|
+
...settings(),
|
|
38
|
+
messageLintRuleLevels,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const messages = messagesQuery.getAll() as Message[]
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
createEffect(() => {
|
|
45
|
+
if (rulesArray) {
|
|
46
|
+
for (const messageId of messagesQuery.includedMessageIds()) {
|
|
47
|
+
createEffect(() => {
|
|
48
|
+
const message = messagesQuery.get({ where: { id: messageId } })
|
|
49
|
+
if (hasWatcher) {
|
|
50
|
+
lintSingleMessage({
|
|
51
|
+
rules: rulesArray,
|
|
52
|
+
settings: settingsObject(),
|
|
53
|
+
messages: messages,
|
|
54
|
+
message: message,
|
|
55
|
+
}).then((report) => {
|
|
56
|
+
if (report.errors.length === 0 && index.get(messageId) !== report.data) {
|
|
57
|
+
index.set(messageId, report.data)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
} else {
|
|
61
|
+
debounce(
|
|
62
|
+
500,
|
|
63
|
+
(message) => {
|
|
64
|
+
lintSingleMessage({
|
|
65
|
+
rules: rulesArray,
|
|
66
|
+
settings: settingsObject(),
|
|
67
|
+
messages: messages,
|
|
68
|
+
message: message,
|
|
69
|
+
}).then((report) => {
|
|
70
|
+
if (report.errors.length === 0 && index.get(messageId) !== report.data) {
|
|
71
|
+
index.set(messageId, report.data)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
},
|
|
75
|
+
{ atBegin: false }
|
|
76
|
+
)(message)
|
|
48
77
|
}
|
|
49
78
|
})
|
|
50
79
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { generateProjectId } from "./generateProjectId.js"
|
|
2
|
+
import { describe, it, expect } from "vitest"
|
|
3
|
+
import { openRepository } from "@lix-js/client/src/openRepository.ts"
|
|
4
|
+
import { mockRepo, createNodeishMemoryFs } from "@lix-js/client"
|
|
5
|
+
|
|
6
|
+
describe("generateProjectId", async () => {
|
|
7
|
+
const repo = await mockRepo()
|
|
8
|
+
|
|
9
|
+
it("should generate a project id", async () => {
|
|
10
|
+
const projectId = await generateProjectId(repo, "mocked_project_path")
|
|
11
|
+
expect(projectId).toBe("0c83325bf9068eb01091c522d4b8e3765aff42e36fc781c041b44439bbe3e734")
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it("should return undefined if repoMeta contains error", async () => {
|
|
15
|
+
const errorRepo = await openRepository("https://github.com/inlang/no-exist", {
|
|
16
|
+
nodeishFs: createNodeishMemoryFs(),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const projectId = await generateProjectId(errorRepo, "mocked_project_path")
|
|
20
|
+
expect(projectId).toBeUndefined()
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Repository } from "@lix-js/client"
|
|
2
|
+
import { hash } from "@lix-js/client"
|
|
3
|
+
|
|
4
|
+
export async function generateProjectId(repo: Repository, projectPath: string) {
|
|
5
|
+
if (!repo || !projectPath) {
|
|
6
|
+
return undefined
|
|
7
|
+
}
|
|
8
|
+
const repoMeta = await repo.getMeta()
|
|
9
|
+
|
|
10
|
+
if (repoMeta && !("error" in repoMeta)) {
|
|
11
|
+
return hash(`${repoMeta.id + projectPath}`)
|
|
12
|
+
}
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type {
|
|
|
13
13
|
} from "./api.js"
|
|
14
14
|
export { type ImportFunction, createImport } from "./resolve-modules/index.js"
|
|
15
15
|
export { loadProject } from "./loadProject.js"
|
|
16
|
+
export { listProjects } from "./listProjects.js"
|
|
16
17
|
export { solidAdapter, type InlangProjectWithSolidAdapter } from "./adapter/solidAdapter.js"
|
|
17
18
|
export { createMessagesQuery } from "./createMessagesQuery.js"
|
|
18
19
|
export {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { assert, describe, it } from "vitest"
|
|
2
|
+
import { listProjects } from "./listProjects.js"
|
|
3
|
+
import { createNodeishMemoryFs } from "@lix-js/fs"
|
|
4
|
+
import type { ProjectSettings } from "@inlang/project-settings"
|
|
5
|
+
|
|
6
|
+
const settings: ProjectSettings = {
|
|
7
|
+
sourceLanguageTag: "en",
|
|
8
|
+
languageTags: ["en"],
|
|
9
|
+
modules: ["plugin.js", "lintRule.js"],
|
|
10
|
+
messageLintRuleLevels: {
|
|
11
|
+
"messageLintRule.project.missingTranslation": "error",
|
|
12
|
+
},
|
|
13
|
+
"plugin.project.i18next": {
|
|
14
|
+
pathPattern: "./examples/example01/{languageTag}.json",
|
|
15
|
+
variableReferencePattern: ["{", "}"],
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("listProjects", () => {
|
|
20
|
+
it("should find all projects a given path", async () => {
|
|
21
|
+
const fs = createNodeishMemoryFs()
|
|
22
|
+
await fs.mkdir("/user/dir1/project.inlang", { recursive: true })
|
|
23
|
+
await fs.writeFile("/user/dir1/project.inlang/settings.json", JSON.stringify(settings))
|
|
24
|
+
await fs.mkdir("/user/dir2/project.inlang", { recursive: true })
|
|
25
|
+
await fs.writeFile("/user/dir2/project.inlang/settings.json", JSON.stringify(settings))
|
|
26
|
+
|
|
27
|
+
await listProjects(fs, "/user").then((projects) => {
|
|
28
|
+
assert(projects.length === 2)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("should return objects inside of an array with the projectPath", async () => {
|
|
33
|
+
const fs = createNodeishMemoryFs()
|
|
34
|
+
await fs.mkdir("/user/dir1/project.inlang", { recursive: true })
|
|
35
|
+
await fs.writeFile("/user/dir1/project.inlang/settings.json", JSON.stringify(settings))
|
|
36
|
+
await fs.mkdir("/user/dir2/project.inlang", { recursive: true })
|
|
37
|
+
await fs.writeFile("/user/dir2/project.inlang/settings.json", JSON.stringify(settings))
|
|
38
|
+
|
|
39
|
+
await listProjects(fs, "/user").then((projects) => {
|
|
40
|
+
assert.isTrue(typeof projects[0] === "object")
|
|
41
|
+
assert.isTrue(typeof projects[0]?.projectPath === "string")
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("should limit the recursion depth to 5", async () => {
|
|
46
|
+
const fs = createNodeishMemoryFs()
|
|
47
|
+
await fs.mkdir("/user/dir1/dir2/dir3/dir4/dir5/dir6/project.inlang", { recursive: true })
|
|
48
|
+
await fs.writeFile(
|
|
49
|
+
"/user/dir1/dir2/dir3/dir4/dir5/dir6/project.inlang/settings.json",
|
|
50
|
+
JSON.stringify(settings)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
await listProjects(fs, "/user").then((projects) => {
|
|
54
|
+
assert(projects.length === 0)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("should also find files inside of a dir that ends with *.inlang", async () => {
|
|
59
|
+
const fs = createNodeishMemoryFs()
|
|
60
|
+
await fs.mkdir("/user/dir1/go.inlang", { recursive: true })
|
|
61
|
+
await fs.writeFile("/user/dir1/go.inlang/settings.json", JSON.stringify(settings))
|
|
62
|
+
await fs.mkdir("/user/dir2/flutter.inlang", { recursive: true })
|
|
63
|
+
await fs.writeFile("/user/dir2/flutter.inlang/settings.json", JSON.stringify(settings))
|
|
64
|
+
|
|
65
|
+
await listProjects(fs, "/user").then((projects) => {
|
|
66
|
+
assert(projects.length === 2)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { NodeishFilesystem } from "@lix-js/fs"
|
|
2
|
+
|
|
3
|
+
export const listProjects = async (
|
|
4
|
+
nodeishFs: NodeishFilesystem,
|
|
5
|
+
from: string
|
|
6
|
+
): Promise<Array<{ projectPath: string }>> => {
|
|
7
|
+
// !TODO: Remove this limit once we introduce caching
|
|
8
|
+
const recursionLimit = 5
|
|
9
|
+
|
|
10
|
+
const projects: Array<{ projectPath: string }> = []
|
|
11
|
+
|
|
12
|
+
async function searchDir(path: string, depth: number) {
|
|
13
|
+
if (depth > recursionLimit) {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const files = await nodeishFs.readdir(path)
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const filePath = `${path}/${file}`
|
|
20
|
+
const stats = await nodeishFs.stat(filePath)
|
|
21
|
+
if (stats.isDirectory()) {
|
|
22
|
+
if (file === "node_modules") continue
|
|
23
|
+
if (file.endsWith(".inlang")) {
|
|
24
|
+
projects.push({ projectPath: filePath })
|
|
25
|
+
} else {
|
|
26
|
+
await searchDir(filePath, depth + 1)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await searchDir(from, 0)
|
|
33
|
+
|
|
34
|
+
// remove double slashes
|
|
35
|
+
for (const project of projects) {
|
|
36
|
+
project.projectPath = project.projectPath.replace(/\/\//g, "/")
|
|
37
|
+
}
|
|
38
|
+
return projects
|
|
39
|
+
}
|
package/src/loadProject.test.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { createNodeishMemoryFs, normalizePath } from "@lix-js/fs"
|
|
20
20
|
import { createMessage } from "./test-utilities/createMessage.js"
|
|
21
21
|
import { tryCatch } from "@inlang/result"
|
|
22
|
+
import { mockRepo } from "@lix-js/client"
|
|
22
23
|
|
|
23
24
|
// ------------------------------------------------------------------------------------------------
|
|
24
25
|
|
|
@@ -159,6 +160,71 @@ describe("initialization", () => {
|
|
|
159
160
|
expect(result.data).toBeUndefined()
|
|
160
161
|
})
|
|
161
162
|
|
|
163
|
+
it("should generate projectId on missing projectid", async () => {
|
|
164
|
+
const repo = await mockRepo()
|
|
165
|
+
|
|
166
|
+
const existing = await repo.nodeishFs
|
|
167
|
+
.readFile("/project.inlang/project_id", {
|
|
168
|
+
encoding: "utf-8",
|
|
169
|
+
})
|
|
170
|
+
.catch((error) => {
|
|
171
|
+
return { error }
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// @ts-ignore
|
|
175
|
+
expect(existing.error.code).toBe("ENOENT")
|
|
176
|
+
|
|
177
|
+
const result = await tryCatch(() =>
|
|
178
|
+
loadProject({
|
|
179
|
+
projectPath: "/project.inlang",
|
|
180
|
+
nodeishFs: repo.nodeishFs,
|
|
181
|
+
repo,
|
|
182
|
+
_import,
|
|
183
|
+
})
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
const newId = await repo.nodeishFs
|
|
187
|
+
.readFile("/project.inlang/project_id", {
|
|
188
|
+
encoding: "utf-8",
|
|
189
|
+
})
|
|
190
|
+
.catch((error) => {
|
|
191
|
+
return { error }
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
expect(newId).toBe("7cd6c2b7cf12febf99496408917123fdfe158b6bc442914f5fb42aa74346bd50")
|
|
195
|
+
|
|
196
|
+
expect(result.error).toBeUndefined()
|
|
197
|
+
expect(result.data).toBeDefined()
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it("should reuse projectId on existing projectid", async () => {
|
|
201
|
+
const repo = await mockRepo()
|
|
202
|
+
|
|
203
|
+
repo.nodeishFs.writeFile("/project.inlang/project_id", "testId")
|
|
204
|
+
|
|
205
|
+
const result = await tryCatch(() =>
|
|
206
|
+
loadProject({
|
|
207
|
+
projectPath: "/project.inlang",
|
|
208
|
+
nodeishFs: repo.nodeishFs,
|
|
209
|
+
repo,
|
|
210
|
+
_import,
|
|
211
|
+
})
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const newId = await repo.nodeishFs
|
|
215
|
+
.readFile("/project.inlang/project_id", {
|
|
216
|
+
encoding: "utf-8",
|
|
217
|
+
})
|
|
218
|
+
.catch((error) => {
|
|
219
|
+
return { error }
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
expect(newId).toBe("testId")
|
|
223
|
+
|
|
224
|
+
expect(result.error).toBeUndefined()
|
|
225
|
+
expect(result.data).toBeDefined()
|
|
226
|
+
})
|
|
227
|
+
|
|
162
228
|
it("should resolve from a windows path", async () => {
|
|
163
229
|
const fs = createNodeishMemoryFs()
|
|
164
230
|
fs.mkdir("C:\\Users\\user\\project.inlang", { recursive: true })
|
|
@@ -691,7 +757,6 @@ describe("functionality", () => {
|
|
|
691
757
|
},
|
|
692
758
|
],
|
|
693
759
|
},
|
|
694
|
-
|
|
695
760
|
{
|
|
696
761
|
languageTag: "de",
|
|
697
762
|
match: [],
|
package/src/loadProject.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { normalizePath, type NodeishFilesystem } from "@lix-js/fs"
|
|
|
27
27
|
import { isAbsolutePath } from "./isAbsolutePath.js"
|
|
28
28
|
import { createNodeishFsWithWatcher } from "./createNodeishFsWithWatcher.js"
|
|
29
29
|
import { maybeMigrateToDirectory } from "./migrations/migrateToDirectory.js"
|
|
30
|
+
import type { Repository } from "@lix-js/client"
|
|
31
|
+
import { generateProjectId } from "./generateProjectId.js"
|
|
30
32
|
|
|
31
33
|
const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
|
|
32
34
|
|
|
@@ -42,6 +44,7 @@ const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
|
|
|
42
44
|
*/
|
|
43
45
|
export const loadProject = async (args: {
|
|
44
46
|
projectPath: string
|
|
47
|
+
repo?: Repository
|
|
45
48
|
nodeishFs: NodeishFilesystem
|
|
46
49
|
_import?: ImportFunction
|
|
47
50
|
_capture?: (id: string, props: Record<string, unknown>) => void
|
|
@@ -70,6 +73,7 @@ export const loadProject = async (args: {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
// -- load project ------------------------------------------------------
|
|
76
|
+
let idError: Error | undefined
|
|
73
77
|
return await createRoot(async () => {
|
|
74
78
|
const [initialized, markInitAsComplete, markInitAsFailed] = createAwaitable()
|
|
75
79
|
const nodeishFs = createNodeishFsWithAbsolutePaths({
|
|
@@ -77,16 +81,43 @@ export const loadProject = async (args: {
|
|
|
77
81
|
nodeishFs: args.nodeishFs,
|
|
78
82
|
})
|
|
79
83
|
|
|
84
|
+
let projectId: string | undefined
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
projectId = await nodeishFs.readFile(projectPath + "/project_id", {
|
|
88
|
+
encoding: "utf-8",
|
|
89
|
+
})
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// @ts-ignore
|
|
92
|
+
if (error.code === "ENOENT") {
|
|
93
|
+
if (args.repo) {
|
|
94
|
+
projectId = await generateProjectId(args.repo, projectPath)
|
|
95
|
+
if (projectId) {
|
|
96
|
+
await nodeishFs.writeFile(projectPath + "/project_id", projectId)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
idError = error as Error
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
80
104
|
// -- settings ------------------------------------------------------------
|
|
81
105
|
|
|
82
106
|
const [settings, _setSettings] = createSignal<ProjectSettings>()
|
|
83
107
|
createEffect(() => {
|
|
108
|
+
// TODO:
|
|
109
|
+
// if (projectId) {
|
|
110
|
+
// telemetryBrowser.group("project", projectId, {
|
|
111
|
+
// name: projectId,
|
|
112
|
+
// })
|
|
113
|
+
// }
|
|
114
|
+
|
|
84
115
|
loadSettings({ settingsFilePath: projectPath + "/settings.json", nodeishFs })
|
|
85
116
|
.then((settings) => {
|
|
86
117
|
setSettings(settings)
|
|
87
118
|
// rename settings to get a convenient access to the data in Posthog
|
|
88
119
|
const project_settings = settings
|
|
89
|
-
args._capture?.("SDK used settings", { project_settings })
|
|
120
|
+
args._capture?.("SDK used settings", { project_settings, group: projectId })
|
|
90
121
|
})
|
|
91
122
|
.catch((err) => {
|
|
92
123
|
markInitAsFailed(err)
|
|
@@ -137,12 +168,11 @@ export const loadProject = async (args: {
|
|
|
137
168
|
let settingsValue: ProjectSettings
|
|
138
169
|
createEffect(() => (settingsValue = settings()!)) // workaround to not run effects twice (e.g. settings change + modules change) (I'm sure there exists a solid way of doing this, but I haven't found it yet)
|
|
139
170
|
|
|
171
|
+
// please don't use this as source of truth, use the query instead
|
|
172
|
+
// needed for granular linting
|
|
140
173
|
const [messages, setMessages] = createSignal<Message[]>()
|
|
141
174
|
|
|
142
175
|
createEffect(() => {
|
|
143
|
-
const conf = settings()
|
|
144
|
-
if (!conf) return
|
|
145
|
-
|
|
146
176
|
const _resolvedModules = resolvedModules()
|
|
147
177
|
if (!_resolvedModules) return
|
|
148
178
|
|
|
@@ -210,12 +240,16 @@ export const loadProject = async (args: {
|
|
|
210
240
|
|
|
211
241
|
const initializeError: Error | undefined = await initialized.catch((error) => error)
|
|
212
242
|
|
|
243
|
+
const abortController = new AbortController()
|
|
244
|
+
const hasWatcher = nodeishFs.watch("/", { signal: abortController.signal }) !== undefined
|
|
245
|
+
|
|
213
246
|
const messagesQuery = createMessagesQuery(() => messages() || [])
|
|
214
247
|
const lintReportsQuery = createMessageLintReportsQuery(
|
|
215
|
-
|
|
248
|
+
messagesQuery,
|
|
216
249
|
settings as () => ProjectSettings,
|
|
217
250
|
installedMessageLintRules,
|
|
218
|
-
resolvedModules
|
|
251
|
+
resolvedModules,
|
|
252
|
+
hasWatcher
|
|
219
253
|
)
|
|
220
254
|
|
|
221
255
|
const debouncedSave = skipFirst(
|
|
@@ -238,7 +272,7 @@ export const loadProject = async (args: {
|
|
|
238
272
|
if (
|
|
239
273
|
newMessages.length !== 0 &&
|
|
240
274
|
JSON.stringify(newMessages) !== JSON.stringify(messages()) &&
|
|
241
|
-
nodeishFs.watch("/", { signal: abortController.signal })
|
|
275
|
+
nodeishFs.watch("/", { signal: abortController.signal }) !== undefined
|
|
242
276
|
) {
|
|
243
277
|
setMessages(newMessages)
|
|
244
278
|
}
|
|
@@ -258,6 +292,7 @@ export const loadProject = async (args: {
|
|
|
258
292
|
},
|
|
259
293
|
errors: createSubscribable(() => [
|
|
260
294
|
...(initializeError ? [initializeError] : []),
|
|
295
|
+
...(idError ? [idError] : []),
|
|
261
296
|
...(resolvedModules() ? resolvedModules()!.errors : []),
|
|
262
297
|
// have a query error exposed
|
|
263
298
|
//...(lintErrors() ?? []),
|