@inlang/sdk 0.19.0 → 0.20.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 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/develop-app) on inlang.
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(messages: () => Array<Message> | undefined, settings: () => ProjectSettings, installedMessageLintRules: () => Array<InstalledMessageLintRule>, resolvedModules: () => Awaited<ReturnType<typeof resolveModules>> | undefined): InlangProject["query"]["messageLintReports"];
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":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,wBAAwB,EAA8B,MAAM,UAAU,CAAA;AACnG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,KAAK,EAAqB,OAAO,EAAE,MAAM,0BAA0B,CAAA;AAI1E;;GAEG;AACH,wBAAgB,6BAA6B,CAC5C,QAAQ,EAAE,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,SAAS,EAC1C,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,GAC3E,aAAa,CAAC,OAAO,CAAC,CAAC,oBAAoB,CAAC,CAqD9C"}
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(messages, settings, installedMessageLintRules, resolvedModules) {
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
- const modules = resolvedModules();
13
- const _messages = messages();
14
- const _settings = settings();
15
- if (_messages && _settings && modules) {
16
- // index.clear()
17
- for (const message of _messages) {
18
- // TODO: only lint changed messages and update arrays selectively
19
- lintSingleMessage({
20
- rules: modules.messageLintRules,
21
- settings: {
22
- ..._settings,
23
- messageLintRuleLevels: Object.fromEntries(installedMessageLintRules().map((rule) => [rule.id, rule.level])),
24
- },
25
- messages: _messages,
26
- message: message,
27
- }).then((report) => {
28
- if (report.errors.length === 0 &&
29
- JSON.stringify(index.get(message.id)) !== JSON.stringify(report.data)) {
30
- index.set(message.id, report.data || []);
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,3 @@
1
+ import type { Repository } from "@lix-js/client";
2
+ export declare function generateProjectId(repo: Repository, projectPath: string): Promise<string | undefined>;
3
+ //# sourceMappingURL=generateProjectId.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=generateProjectId.test.d.ts.map
@@ -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";
@@ -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,5 @@
1
+ import type { NodeishFilesystem } from "@lix-js/fs";
2
+ export declare const listProjects: (nodeishFs: NodeishFilesystem, from: string) => Promise<Array<{
3
+ projectPath: string;
4
+ }>>;
5
+ //# sourceMappingURL=listProjects.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=listProjects.test.d.ts.map
@@ -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
+ });
@@ -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;AAOlE;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW;iBACV,MAAM;eACR,iBAAiB;;qBAEZ,MAAM,SAAS,OAAO,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI;MAC5D,QAAQ,aAAa,CAkOxB,CAAA;AAwHD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAQtE"}
1
+ {"version":3,"file":"loadProject.d.ts","sourceRoot":"","sources":["../src/loadProject.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,aAAa,EAGb,YAAY,EACZ,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,cAAc,EAAkB,MAAM,4BAA4B,CAAA;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"}
@@ -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(messages, settings, installedMessageLintRules, resolvedModules);
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 }) === undefined) {
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() ?? []),
@@ -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.19.0",
4
+ "version": "0.20.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/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
29
  "@inlang/message": "2.0.0",
30
+ "@inlang/language-tag": "1.2.0",
31
+ "@inlang/message-lint-rule": "1.4.0",
34
32
  "@inlang/plugin": "2.4.0",
35
- "@inlang/result": "1.1.0",
33
+ "@inlang/module": "1.2.0",
36
34
  "@inlang/project-settings": "2.2.0",
37
- "@lix-js/fs": "0.4.0"
35
+ "@inlang/result": "1.1.0",
36
+ "@lix-js/fs": "0.4.0",
37
+ "@lix-js/client": "0.2.0",
38
+ "@inlang/translatable": "1.2.0"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/throttle-debounce": "5.0.0",
@@ -1,50 +1,79 @@
1
- import { createEffect } from "./reactivity/solid.js"
2
1
  import { createSubscribable } from "./loadProject.js"
3
- import type { InlangProject, InstalledMessageLintRule, MessageLintReportsQueryApi } from "./api.js"
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
- messages: () => Array<Message> | undefined,
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
- createEffect(() => {
23
- const modules = resolvedModules()
24
- const _messages = messages()
25
- const _settings = settings()
29
+ const modules = resolvedModules()
26
30
 
27
- if (_messages && _settings && modules) {
28
- // index.clear()
29
- for (const message of _messages) {
30
- // TODO: only lint changed messages and update arrays selectively
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
- lintSingleMessage({
33
- rules: modules.messageLintRules,
34
- settings: {
35
- ..._settings,
36
- messageLintRuleLevels: Object.fromEntries(
37
- installedMessageLintRules().map((rule) => [rule.id, rule.level])
38
- ),
39
- },
40
- messages: _messages,
41
- message: message,
42
- }).then((report) => {
43
- if (
44
- report.errors.length === 0 &&
45
- JSON.stringify(index.get(message.id)) !== JSON.stringify(report.data)
46
- ) {
47
- index.set(message.id, report.data || [])
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
+ }
@@ -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: [],
@@ -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
- messages,
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 }) === undefined
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() ?? []),