@inlang/sdk 0.18.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.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter/solidAdapter.test.js +16 -15
  3. package/dist/createMessageLintReportsQuery.d.ts +2 -3
  4. package/dist/createMessageLintReportsQuery.d.ts.map +1 -1
  5. package/dist/createMessageLintReportsQuery.js +42 -21
  6. package/dist/createNodeishFsWithAbsolutePaths.d.ts +2 -2
  7. package/dist/createNodeishFsWithAbsolutePaths.d.ts.map +1 -1
  8. package/dist/createNodeishFsWithAbsolutePaths.js +4 -4
  9. package/dist/createNodeishFsWithAbsolutePaths.test.js +4 -4
  10. package/dist/createNodeishFsWithWatcher.d.ts +1 -1
  11. package/dist/createNodeishFsWithWatcher.d.ts.map +1 -1
  12. package/dist/createNodeishFsWithWatcher.js +6 -4
  13. package/dist/generateProjectId.d.ts +3 -0
  14. package/dist/generateProjectId.d.ts.map +1 -0
  15. package/dist/generateProjectId.js +11 -0
  16. package/dist/generateProjectId.test.d.ts +2 -0
  17. package/dist/generateProjectId.test.d.ts.map +1 -0
  18. package/dist/generateProjectId.test.js +18 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -0
  22. package/dist/isAbsolutePath.test.js +1 -1
  23. package/dist/listProjects.d.ts +5 -0
  24. package/dist/listProjects.d.ts.map +1 -0
  25. package/dist/listProjects.js +31 -0
  26. package/dist/listProjects.test.d.ts +2 -0
  27. package/dist/listProjects.test.d.ts.map +1 -0
  28. package/dist/listProjects.test.js +56 -0
  29. package/dist/loadProject.d.ts +6 -4
  30. package/dist/loadProject.d.ts.map +1 -1
  31. package/dist/loadProject.js +58 -22
  32. package/dist/loadProject.test.js +157 -75
  33. package/dist/migrations/migrateToDirectory.d.ts +10 -0
  34. package/dist/migrations/migrateToDirectory.d.ts.map +1 -0
  35. package/dist/migrations/migrateToDirectory.js +46 -0
  36. package/dist/migrations/migrateToDirectory.test.d.ts +2 -0
  37. package/dist/migrations/migrateToDirectory.test.d.ts.map +1 -0
  38. package/dist/migrations/migrateToDirectory.test.js +48 -0
  39. package/dist/resolve-modules/validateModuleSettings.test.js +1 -1
  40. package/package.json +57 -56
  41. package/src/adapter/solidAdapter.test.ts +16 -15
  42. package/src/createMessageLintReportsQuery.ts +57 -28
  43. package/src/createNodeishFsWithAbsolutePaths.test.ts +4 -4
  44. package/src/createNodeishFsWithAbsolutePaths.ts +5 -5
  45. package/src/createNodeishFsWithWatcher.ts +6 -4
  46. package/src/generateProjectId.test.ts +22 -0
  47. package/src/generateProjectId.ts +14 -0
  48. package/src/index.ts +1 -0
  49. package/src/isAbsolutePath.test.ts +1 -1
  50. package/src/listProjects.test.ts +69 -0
  51. package/src/listProjects.ts +39 -0
  52. package/src/loadProject.test.ts +182 -76
  53. package/src/loadProject.ts +77 -28
  54. package/src/migrations/migrateToDirectory.test.ts +54 -0
  55. package/src/migrations/migrateToDirectory.ts +59 -0
  56. package/src/resolve-modules/validateModuleSettings.test.ts +1 -1
@@ -0,0 +1,46 @@
1
+ import { tryCatch } from "@inlang/result";
2
+ /**
3
+ * Migrates to the new project directory structure
4
+ * https://github.com/inlang/monorepo/issues/1678
5
+ */
6
+ export const maybeMigrateToDirectory = async (args) => {
7
+ // the migration assumes that the projectPath ends with project.inlang
8
+ if (args.projectPath.endsWith("project.inlang") === false) {
9
+ return;
10
+ }
11
+ // we assume that stat will throw when the directory does not exist
12
+ const projectDirectory = await tryCatch(() => args.nodeishFs.stat(args.projectPath));
13
+ // the migration has already been conducted.
14
+ if (projectDirectory.data) {
15
+ return;
16
+ }
17
+ const settingsFile = await tryCatch(() => args.nodeishFs.readFile(args.projectPath + ".json", { encoding: "utf-8" }));
18
+ // the settings file does not exist or something else is wrong, let loadProject handle it
19
+ if (settingsFile.error) {
20
+ return;
21
+ }
22
+ await args.nodeishFs.mkdir(args.projectPath);
23
+ await args.nodeishFs.writeFile(`${args.projectPath}/settings.json`, settingsFile.data);
24
+ await args.nodeishFs.writeFile(args.projectPath + ".README.md", readme);
25
+ };
26
+ const readme = `
27
+ # DELETE THE \`project.inlang.json\` FILE
28
+
29
+ The \`project.inlang.json\` file is now contained in a project directory e.g. \`project.inlang/settings.json\`.
30
+
31
+
32
+ ## What you need to do
33
+
34
+ 1. Update the inlang CLI (if you use it) to use the new path \`project.inlang\` instead of \`project.inlang.json\`.
35
+ 2. Delete the \`project.inlang.json\` file.
36
+
37
+
38
+ ## Why is this happening?
39
+
40
+ See this RFC https://docs.google.com/document/d/1OYyA1wYfQRbIJOIBDliYoWjiUlkFBNxH_U2R4WpVRZ4/edit#heading=h.pecv6xb7ial6
41
+ and the following GitHub issue for more information https://github.com/inlang/monorepo/issues/1678.
42
+
43
+ - Monorepo support https://github.com/inlang/monorepo/discussions/258.
44
+ - Required for many other future features like caching, first class offline support, and more.
45
+ - Stablize the inlang project format.
46
+ `;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=migrateToDirectory.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrateToDirectory.test.d.ts","sourceRoot":"","sources":["../../src/migrations/migrateToDirectory.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,48 @@
1
+ import { test, expect, vi } from "vitest";
2
+ import { maybeMigrateToDirectory } from "./migrateToDirectory.js";
3
+ import { createNodeishMemoryFs } from "@lix-js/fs";
4
+ test("it should return if the settings file has an error (let loadProject handle it)", async () => {
5
+ const projectPath = "./project.inlang";
6
+ const mockFs = {
7
+ stat: vi.fn(() => { }),
8
+ readFile: vi.fn(() => {
9
+ throw Error();
10
+ }),
11
+ };
12
+ await maybeMigrateToDirectory({ nodeishFs: mockFs, projectPath });
13
+ // something goes wrong in readFile
14
+ expect(mockFs.readFile).toHaveBeenCalled();
15
+ });
16
+ test("it should create the project directory if it does not exist and a project settings file exists", async () => {
17
+ const projectPath = "./project.inlang";
18
+ const mockFs = {
19
+ readFile: vi.fn(() => `{
20
+ "sourceLanguageTag": "en",
21
+ "languageTags": ["en", "de"],
22
+ "modules": []
23
+ }`),
24
+ stat: vi.fn(() => {
25
+ throw Error();
26
+ }),
27
+ mkdir: vi.fn(),
28
+ writeFile: vi.fn(),
29
+ };
30
+ await maybeMigrateToDirectory({ nodeishFs: mockFs, projectPath });
31
+ expect(mockFs.mkdir).toHaveBeenCalled();
32
+ expect(mockFs.writeFile).toHaveBeenCalled();
33
+ });
34
+ test("it should write the settings file to the new path", async () => {
35
+ const fs = createNodeishMemoryFs();
36
+ const mockSettings = {
37
+ sourceLanguageTag: "en",
38
+ languageTags: ["en", "de"],
39
+ modules: [],
40
+ };
41
+ await fs.writeFile("./project.inlang.json", JSON.stringify(mockSettings));
42
+ await maybeMigrateToDirectory({ nodeishFs: fs, projectPath: "./project.inlang" });
43
+ const migratedSettingsFile = await fs.readFile("./project.inlang/settings.json", {
44
+ encoding: "utf-8",
45
+ });
46
+ expect(migratedSettingsFile).toEqual(JSON.stringify(mockSettings));
47
+ expect(await fs.stat("./project.inlang.README.md")).toBeDefined();
48
+ });
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
2
  import { expect, test } from "vitest";
3
- import { Plugin, MessageLintRule } from "@inlang/sdk";
3
+ import { Plugin, MessageLintRule } from "../index.js";
4
4
  // import { createNodeishMemoryFs } from "@lix-js/fs"
5
5
  import { Type } from "@sinclair/typebox";
6
6
  import { validatedModuleSettings } from "./validatedModuleSettings.js";
package/package.json CHANGED
@@ -1,57 +1,58 @@
1
1
  {
2
- "name": "@inlang/sdk",
3
- "type": "module",
4
- "version": "0.18.0",
5
- "license": "Apache-2.0",
6
- "publishConfig": {
7
- "access": "public"
8
- },
9
- "exports": {
10
- ".": "./dist/index.js",
11
- "./test-utilities": "./dist/test-utilities/index.js",
12
- "./lint": "./dist/lint/index.js",
13
- "./messages": "./dist/messages/index.js"
14
- },
15
- "files": [
16
- "./dist",
17
- "./src"
18
- ],
19
- "scripts": {
20
- "build": "tsc --build",
21
- "dev": "tsc --watch",
22
- "test": "tsc --noEmit && vitest run --passWithNoTests --coverage",
23
- "lint": "eslint ./src --fix",
24
- "format": "prettier ./src --write",
25
- "clean": "rm -rf ./dist ./node_modules"
26
- },
27
- "engines": {
28
- "node": ">=18.0.0"
29
- },
30
- "dependencies": {
31
- "@inlang/json-types": "*",
32
- "@inlang/translatable": "*",
33
- "@inlang/message-lint-rule": "*",
34
- "@inlang/module": "*",
35
- "@inlang/language-tag": "*",
36
- "@inlang/message": "*",
37
- "@inlang/plugin": "*",
38
- "@inlang/project-settings": "*",
39
- "@inlang/result": "*",
40
- "@lix-js/fs": "*",
41
- "@sinclair/typebox": "^0.31.17",
42
- "deepmerge-ts": "^5.1.0",
43
- "solid-js": "1.6.12",
44
- "throttle-debounce": "5.0.0",
45
- "dedent": "1.5.1"
46
- },
47
- "devDependencies": {
48
- "@lix-js/fs": "*",
49
- "@types/throttle-debounce": "5.0.0",
50
- "@vitest/coverage-v8": "^0.33.0",
51
- "jsdom": "22.1.0",
52
- "patch-package": "6.5.1",
53
- "tsd": "^0.25.0",
54
- "typescript": "5.2.2",
55
- "vitest": "^0.33.0"
56
- }
57
- }
2
+ "name": "@inlang/sdk",
3
+ "type": "module",
4
+ "version": "0.20.0",
5
+ "license": "Apache-2.0",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "exports": {
10
+ ".": "./dist/index.js",
11
+ "./test-utilities": "./dist/test-utilities/index.js",
12
+ "./lint": "./dist/lint/index.js",
13
+ "./messages": "./dist/messages/index.js"
14
+ },
15
+ "files": [
16
+ "./dist",
17
+ "./src"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18.0.0"
21
+ },
22
+ "dependencies": {
23
+ "@sinclair/typebox": "^0.31.17",
24
+ "dedent": "1.5.1",
25
+ "deepmerge-ts": "^5.1.0",
26
+ "solid-js": "1.6.12",
27
+ "throttle-debounce": "^5.0.0",
28
+ "@inlang/json-types": "1.1.0",
29
+ "@inlang/message": "2.0.0",
30
+ "@inlang/language-tag": "1.2.0",
31
+ "@inlang/message-lint-rule": "1.4.0",
32
+ "@inlang/plugin": "2.4.0",
33
+ "@inlang/module": "1.2.0",
34
+ "@inlang/project-settings": "2.2.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"
39
+ },
40
+ "devDependencies": {
41
+ "@types/throttle-debounce": "5.0.0",
42
+ "@vitest/coverage-v8": "^0.33.0",
43
+ "jsdom": "22.1.0",
44
+ "patch-package": "6.5.1",
45
+ "tsd": "^0.25.0",
46
+ "typescript": "5.2.2",
47
+ "vitest": "^0.33.0",
48
+ "@lix-js/fs": "0.4.0"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc --build",
52
+ "dev": "tsc --watch",
53
+ "test": "tsc --noEmit && vitest run --passWithNoTests --coverage",
54
+ "lint": "eslint ./src --fix",
55
+ "format": "prettier ./src --write",
56
+ "clean": "rm -rf ./dist ./node_modules"
57
+ }
58
+ }
@@ -90,11 +90,11 @@ const $import: ImportFunction = async (name) => ({
90
90
  describe("config", () => {
91
91
  it("should react to changes in config", async () => {
92
92
  const fs = createNodeishMemoryFs()
93
- await fs.mkdir("/user/project", { recursive: true })
94
- await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(config))
93
+ await fs.mkdir("/user/project.inlang", { recursive: true })
94
+ await fs.writeFile("/user/project.inlang/settings.json", JSON.stringify(config))
95
95
  const project = solidAdapter(
96
96
  await loadProject({
97
- settingsFilePath: "/user/project/project.inlang.json",
97
+ projectPath: "/user/project.inlang",
98
98
  nodeishFs: fs,
99
99
  _import: $import,
100
100
  }),
@@ -123,11 +123,11 @@ describe("config", () => {
123
123
  describe("installed", () => {
124
124
  it("react to changes that are unrelated to installed items", async () => {
125
125
  const fs = createNodeishMemoryFs()
126
- await fs.mkdir("/user/project", { recursive: true })
127
- await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(config))
126
+ await fs.mkdir("/user/project.inlang", { recursive: true })
127
+ await fs.writeFile("/user/project.inlang/settings.json", JSON.stringify(config))
128
128
  const project = solidAdapter(
129
129
  await loadProject({
130
- settingsFilePath: "/user/project/project.inlang.json",
130
+ projectPath: "/user/project.inlang",
131
131
  nodeishFs: fs,
132
132
  _import: $import,
133
133
  }),
@@ -188,11 +188,11 @@ describe("messages", () => {
188
188
 
189
189
  const mockImport: ImportFunction = async () => ({ default: mockPlugin })
190
190
 
191
- await fs.mkdir("/user/project", { recursive: true })
192
- await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(mockConfig))
191
+ await fs.mkdir("/user/project.inlang.inlang", { recursive: true })
192
+ await fs.writeFile("/user/project.inlang.inlang/settings.json", JSON.stringify(mockConfig))
193
193
  const project = solidAdapter(
194
194
  await loadProject({
195
- settingsFilePath: "/user/project/project.inlang.json",
195
+ projectPath: "/user/project.inlang.inlang",
196
196
  nodeishFs: fs,
197
197
  _import: mockImport,
198
198
  }),
@@ -218,11 +218,11 @@ describe("messages", () => {
218
218
 
219
219
  it("should react to changes in messages", async () => {
220
220
  const fs = createNodeishMemoryFs()
221
- await fs.mkdir("/user/project", { recursive: true })
222
- await fs.writeFile("/user/project/project.inlang.json", JSON.stringify(config))
221
+ await fs.mkdir("/user/project.inlang.inlang", { recursive: true })
222
+ await fs.writeFile("/user/project.inlang.inlang/settings.json", JSON.stringify(config))
223
223
  const project = solidAdapter(
224
224
  await loadProject({
225
- settingsFilePath: "/user/project/project.inlang.json",
225
+ projectPath: "/user/project.inlang.inlang",
226
226
  nodeishFs: fs,
227
227
  _import: $import,
228
228
  }),
@@ -280,10 +280,11 @@ describe("lint", () => {
280
280
  it.todo("should react to changes in config", async () => {
281
281
  await createRoot(async () => {
282
282
  const fs = createNodeishMemoryFs()
283
- await fs.writeFile("./project.config.json", JSON.stringify(config))
283
+ await fs.mkdir("./project.inlang", { recursive: true })
284
+ await fs.writeFile("./project.inlang/settings.json", JSON.stringify(config))
284
285
  const project = solidAdapter(
285
286
  await loadProject({
286
- settingsFilePath: "./project.config.json",
287
+ projectPath: "./project.inlang",
287
288
  nodeishFs: fs,
288
289
  _import: $import,
289
290
  }),
@@ -321,7 +322,7 @@ describe("lint", () => {
321
322
  await fs.writeFile("./project.config.json", JSON.stringify(config))
322
323
  const project = solidAdapter(
323
324
  await loadProject({
324
- settingsFilePath: "./project.config.json",
325
+ projectPath: "./project.config.json",
325
326
  nodeishFs: fs,
326
327
  _import: $import,
327
328
  }),
@@ -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
  }
@@ -2,16 +2,16 @@ import { it, expect, vi } from "vitest"
2
2
  import { createNodeishFsWithAbsolutePaths } from "./createNodeishFsWithAbsolutePaths.js"
3
3
  import type { NodeishFilesystemSubset } from "./versionedInterfaces.js"
4
4
 
5
- it("throws an error if settingsFilePath is not an absolute path", () => {
5
+ it("throws an error if projectPath is not an absolute path", () => {
6
6
  const relativePath = "relative/path"
7
7
 
8
8
  expect(() =>
9
- createNodeishFsWithAbsolutePaths({ settingsFilePath: relativePath, nodeishFs: {} as any })
9
+ createNodeishFsWithAbsolutePaths({ projectPath: relativePath, nodeishFs: {} as any })
10
10
  ).toThrow()
11
11
  })
12
12
 
13
13
  it("intercepts paths correctly for readFile", async () => {
14
- const settingsFilePath = `/Users/samuel/Documents/paraglide/example/project.inlang.json`
14
+ const projectPath = `/Users/samuel/Documents/paraglide/example/project.inlang`
15
15
 
16
16
  const filePaths = [
17
17
  ["file.txt", `/Users/samuel/Documents/paraglide/example/file.txt`],
@@ -32,7 +32,7 @@ it("intercepts paths correctly for readFile", async () => {
32
32
  } satisfies Record<keyof NodeishFilesystemSubset, any>
33
33
 
34
34
  const interceptedFs = createNodeishFsWithAbsolutePaths({
35
- settingsFilePath,
35
+ projectPath,
36
36
  nodeishFs: mockNodeishFs,
37
37
  })
38
38
 
@@ -6,19 +6,19 @@ import { isAbsolutePath } from "./isAbsolutePath.js"
6
6
  * Wraps the nodeish filesystem subset with a function that intercepts paths
7
7
  * and prepends the base path.
8
8
  *
9
- * The paths are resolved from the `settingsFilePath` argument.
9
+ * The paths are resolved from the `projectPath` argument.
10
10
  */
11
11
  export const createNodeishFsWithAbsolutePaths = (args: {
12
- settingsFilePath: string
12
+ projectPath: string
13
13
  nodeishFs: NodeishFilesystemSubset
14
14
  }): NodeishFilesystemSubset => {
15
- if (!isAbsolutePath(args.settingsFilePath)) {
16
- throw new Error(`Expected an absolute path but received "${args.settingsFilePath}".`)
15
+ if (!isAbsolutePath(args.projectPath)) {
16
+ throw new Error(`Expected an absolute path but received "${args.projectPath}".`)
17
17
  }
18
18
 
19
19
  // get the base path of the settings file by
20
20
  // removing the file name from the path
21
- const basePath = normalizePath(args.settingsFilePath).split("/").slice(0, -1).join("/")
21
+ const basePath = normalizePath(args.projectPath).split("/").slice(0, -1).join("/")
22
22
 
23
23
  const makeAbsolute = (path: string) => {
24
24
  if (isAbsolutePath(path)) {
@@ -4,7 +4,7 @@ import type { NodeishFilesystemSubset } from "@inlang/plugin"
4
4
  * Wraps the nodeish filesystem subset with a function that intercepts paths
5
5
  * and prepends the base path.
6
6
  *
7
- * The paths are resolved from the `settingsFilePath` argument.
7
+ * The paths are resolved from the `projectPath` argument.
8
8
  */
9
9
  export const createNodeishFsWithWatcher = (args: {
10
10
  nodeishFs: NodeishFilesystemSubset
@@ -20,9 +20,11 @@ export const createNodeishFsWithWatcher = (args: {
20
20
  signal: abortController.signal,
21
21
  persistent: false,
22
22
  })
23
- //eslint-disable-next-line @typescript-eslint/no-unused-vars
24
- for await (const event of watcher) {
25
- args.updateMessages()
23
+ if (watcher) {
24
+ //eslint-disable-next-line @typescript-eslint/no-unused-vars
25
+ for await (const event of watcher) {
26
+ args.updateMessages()
27
+ }
26
28
  }
27
29
  } catch (err: any) {
28
30
  if (err.name === "AbortError") return
@@ -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 {
@@ -10,7 +10,7 @@ describe("isAbsolutePath", () => {
10
10
 
11
11
  it("should correctly identify Windows absolute paths", () => {
12
12
  assert.isTrue(isAbsolutePath("C:\\Users\\User\\Documents\\File.txt"))
13
- assert.isTrue(isAbsolutePath("C:/Users/user/project/project.inlang.json"))
13
+ assert.isTrue(isAbsolutePath("C:/Users/user/project.inlang/settings.json"))
14
14
  assert.isFalse(isAbsolutePath("Projects\\Project1\\source\\file.txt"))
15
15
  })
16
16
 
@@ -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
+ })