@inlang/sdk 0.32.0 → 0.33.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 (42) hide show
  1. package/dist/createNewProject.d.ts +17 -0
  2. package/dist/createNewProject.d.ts.map +1 -0
  3. package/dist/createNewProject.js +22 -0
  4. package/dist/createNewProject.test.d.ts +2 -0
  5. package/dist/createNewProject.test.d.ts.map +1 -0
  6. package/dist/createNewProject.test.js +93 -0
  7. package/dist/createNodeishFsWithAbsolutePaths.js +1 -1
  8. package/dist/defaultProjectSettings.d.ts +14 -0
  9. package/dist/defaultProjectSettings.d.ts.map +1 -0
  10. package/dist/defaultProjectSettings.js +23 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -0
  14. package/dist/loadProject.d.ts.map +1 -1
  15. package/dist/loadProject.js +3 -8
  16. package/dist/loadProject.test.js +4 -4
  17. package/dist/migrations/maybeCreateFirstProjectId.test.js +3 -6
  18. package/dist/validateProjectPath.d.ts +23 -0
  19. package/dist/validateProjectPath.d.ts.map +1 -0
  20. package/dist/validateProjectPath.js +52 -0
  21. package/dist/validateProjectPath.test.d.ts +2 -0
  22. package/dist/validateProjectPath.test.d.ts.map +1 -0
  23. package/dist/validateProjectPath.test.js +56 -0
  24. package/package.json +4 -4
  25. package/src/createNewProject.test.ts +108 -0
  26. package/src/createNewProject.ts +31 -0
  27. package/src/createNodeishFsWithAbsolutePaths.ts +1 -1
  28. package/src/defaultProjectSettings.ts +27 -0
  29. package/src/index.ts +2 -0
  30. package/src/loadProject.test.ts +5 -4
  31. package/src/loadProject.ts +2 -13
  32. package/src/migrations/maybeCreateFirstProjectId.test.ts +4 -7
  33. package/src/validateProjectPath.test.ts +68 -0
  34. package/src/validateProjectPath.ts +58 -0
  35. package/dist/isAbsolutePath.d.ts +0 -2
  36. package/dist/isAbsolutePath.d.ts.map +0 -1
  37. package/dist/isAbsolutePath.js +0 -4
  38. package/dist/isAbsolutePath.test.d.ts +0 -2
  39. package/dist/isAbsolutePath.test.d.ts.map +0 -1
  40. package/dist/isAbsolutePath.test.js +0 -20
  41. package/src/isAbsolutePath.test.ts +0 -23
  42. package/src/isAbsolutePath.ts +0 -5
@@ -0,0 +1,17 @@
1
+ import type { Repository } from "@lix-js/client";
2
+ import { ProjectSettings } from "@inlang/project-settings";
3
+ /**
4
+ * Creates a new project in the given directory.
5
+ * The directory must be an absolute path, must not exist, and must end with {name}.inlang
6
+ * Uses defaultProjectSettings unless projectSettings are provided.
7
+ *
8
+ * @param projectPath - Absolute path to the [name].inlang directory
9
+ * @param repo - An instance of a lix repo as returned by `openRepository`
10
+ * @param projectSettings - Optional project settings to use for the new project.
11
+ */
12
+ export declare function createNewProject(args: {
13
+ projectPath: string;
14
+ repo: Repository;
15
+ projectSettings: ProjectSettings;
16
+ }): Promise<void>;
17
+ //# sourceMappingURL=createNewProject.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createNewProject.d.ts","sourceRoot":"","sources":["../src/createNewProject.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAI1D;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC5C,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,UAAU,CAAA;IAChB,eAAe,EAAE,eAAe,CAAA;CAChC,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB"}
@@ -0,0 +1,22 @@
1
+ import { ProjectSettings } from "@inlang/project-settings";
2
+ import { assertValidProjectPath, pathExists } from "./validateProjectPath.js";
3
+ import { defaultProjectSettings } from "./defaultProjectSettings.js";
4
+ /**
5
+ * Creates a new project in the given directory.
6
+ * The directory must be an absolute path, must not exist, and must end with {name}.inlang
7
+ * Uses defaultProjectSettings unless projectSettings are provided.
8
+ *
9
+ * @param projectPath - Absolute path to the [name].inlang directory
10
+ * @param repo - An instance of a lix repo as returned by `openRepository`
11
+ * @param projectSettings - Optional project settings to use for the new project.
12
+ */
13
+ export async function createNewProject(args) {
14
+ assertValidProjectPath(args.projectPath);
15
+ const nodeishFs = args.repo.nodeishFs;
16
+ if (await pathExists(args.projectPath, nodeishFs)) {
17
+ throw new Error(`projectPath already exists, received "${args.projectPath}"`);
18
+ }
19
+ await nodeishFs.mkdir(args.projectPath, { recursive: true });
20
+ const settingsText = JSON.stringify(args.projectSettings ?? defaultProjectSettings, undefined, 2);
21
+ await nodeishFs.writeFile(`${args.projectPath}/settings.json`, settingsText);
22
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=createNewProject.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createNewProject.test.d.ts","sourceRoot":"","sources":["../src/createNewProject.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createNewProject } from "./createNewProject.js";
3
+ import { mockRepo } from "@lix-js/client";
4
+ import { defaultProjectSettings } from "./defaultProjectSettings.js";
5
+ import { loadProject } from "./loadProject.js";
6
+ import { createMessage } from "./test-utilities/createMessage.js";
7
+ function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+ describe("createNewProject", () => {
11
+ it("should throw if a path does not end with .inlang", async () => {
12
+ const repo = await mockRepo();
13
+ const projectPath = "/test/project.inl";
14
+ try {
15
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
16
+ // should not reach this point
17
+ throw new Error("Expected an error");
18
+ }
19
+ catch (e) {
20
+ expect(e.message).toMatch('Expected a path ending in "{name}.inlang" but received "/test/project.inl"');
21
+ }
22
+ });
23
+ it("should throw if projectPath is not an absolute path", async () => {
24
+ const repo = await mockRepo();
25
+ const projectPath = "test/project.inlang";
26
+ try {
27
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
28
+ // should not reach this point
29
+ throw new Error("Expected an error");
30
+ }
31
+ catch (e) {
32
+ expect(e.message).toMatch('Expected an absolute path but received "test/project.inlang"');
33
+ }
34
+ });
35
+ it("should throw if the path already exists", async () => {
36
+ const repo = await mockRepo();
37
+ const projectPath = "/test/project.inlang";
38
+ await repo.nodeishFs.mkdir(projectPath, { recursive: true });
39
+ try {
40
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
41
+ // should not reach this point
42
+ throw new Error("Expected an error");
43
+ }
44
+ catch (e) {
45
+ expect(e.message).toMatch('projectPath already exists, received "/test/project.inlang"');
46
+ }
47
+ });
48
+ it("should create default defaultProjectSettings in projectPath", async () => {
49
+ const repo = await mockRepo();
50
+ const projectPath = "/test/project.inlang";
51
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
52
+ const json = await repo.nodeishFs.readFile(`${projectPath}/settings.json`, {
53
+ encoding: "utf-8",
54
+ });
55
+ const settings = JSON.parse(json);
56
+ expect(settings).toEqual(defaultProjectSettings);
57
+ });
58
+ it("should create different projectSettings in projectPath", async () => {
59
+ const repo = await mockRepo();
60
+ const projectPath = "/test/project.inlang";
61
+ const projectSettings = { ...defaultProjectSettings, languageTags: ["en", "de", "fr"] };
62
+ await createNewProject({ projectPath, repo, projectSettings });
63
+ const json = await repo.nodeishFs.readFile(`${projectPath}/settings.json`, {
64
+ encoding: "utf-8",
65
+ });
66
+ const settings = JSON.parse(json);
67
+ expect(settings).toEqual(projectSettings);
68
+ expect(settings).not.toEqual(defaultProjectSettings);
69
+ });
70
+ it("should load the project after creating it", async () => {
71
+ const repo = await mockRepo();
72
+ const projectPath = "/test/project.inlang";
73
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
74
+ const project = await loadProject({ projectPath, repo });
75
+ expect(project.errors().length).toBe(0);
76
+ });
77
+ it("should create messages inside the project directory", async () => {
78
+ const repo = await mockRepo();
79
+ const projectPath = "/test/project.inlang";
80
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
81
+ const project = await loadProject({ projectPath, repo });
82
+ expect(project.errors().length).toBe(0);
83
+ const testMessage = createMessage("test", { en: "test message" });
84
+ project.query.messages.create({ data: testMessage });
85
+ const messages = project.query.messages.getAll();
86
+ expect(messages.length).toBe(1);
87
+ expect(messages[0]).toEqual(testMessage);
88
+ await sleep(20);
89
+ const json = await repo.nodeishFs.readFile("/test/messages/en.json", { encoding: "utf-8" });
90
+ const jsonMessages = JSON.parse(json);
91
+ expect(jsonMessages["test"]).toEqual("test message");
92
+ });
93
+ });
@@ -1,5 +1,5 @@
1
1
  import { normalizePath } from "@lix-js/fs";
2
- import { isAbsolutePath } from "./isAbsolutePath.js";
2
+ import { isAbsolutePath } from "./validateProjectPath.js";
3
3
  /**
4
4
  * Wraps the nodeish filesystem subset with a function that intercepts paths
5
5
  * and prepends the base path.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Default project settings for createNewProject
3
+ * from paraglide-js/src/cli/commands/init/defaults.ts
4
+ */
5
+ export declare const defaultProjectSettings: {
6
+ $schema: "https://inlang.com/schema/project-settings";
7
+ sourceLanguageTag: string;
8
+ languageTags: string[];
9
+ modules: string[];
10
+ "plugin.inlang.messageFormat": {
11
+ pathPattern: string;
12
+ };
13
+ };
14
+ //# sourceMappingURL=defaultProjectSettings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaultProjectSettings.d.ts","sourceRoot":"","sources":["../src/defaultProjectSettings.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;CAoBR,CAAA"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Default project settings for createNewProject
3
+ * from paraglide-js/src/cli/commands/init/defaults.ts
4
+ */
5
+ export const defaultProjectSettings = {
6
+ $schema: "https://inlang.com/schema/project-settings",
7
+ sourceLanguageTag: "en",
8
+ languageTags: ["en"],
9
+ modules: [
10
+ // for instant gratification, we're adding common rules
11
+ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
12
+ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js",
13
+ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js",
14
+ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@latest/dist/index.js",
15
+ // default to the message format plugin because it supports all features
16
+ "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js",
17
+ // the m function matcher should be installed by default in case Sherlock (VS Code extension) is adopted
18
+ "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js",
19
+ ],
20
+ "plugin.inlang.messageFormat": {
21
+ pathPattern: "./messages/{languageTag}.json",
22
+ },
23
+ };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  */
6
6
  export type { InlangProject, InstalledMessageLintRule, InstalledPlugin, MessageQueryApi, Subscribable, } from "./api.js";
7
7
  export { type ImportFunction, createImport } from "./resolve-modules/index.js";
8
+ export { createNewProject } from "./createNewProject.js";
9
+ export { defaultProjectSettings } from "./defaultProjectSettings.js";
8
10
  export { loadProject } from "./loadProject.js";
9
11
  export { listProjects } from "./listProjects.js";
10
12
  export { solidAdapter, type InlangProjectWithSolidAdapter } from "./adapter/solidAdapter.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,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"}
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,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AACpE,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
@@ -4,6 +4,8 @@
4
4
  *! EXPORT AS LITTLE AS POSSIBLE TO MINIMIZE THE CHANCE OF BREAKING CHANGES.
5
5
  */
6
6
  export { createImport } from "./resolve-modules/index.js";
7
+ export { createNewProject } from "./createNewProject.js";
8
+ export { defaultProjectSettings } from "./defaultProjectSettings.js";
7
9
  export { loadProject } from "./loadProject.js";
8
10
  export { listProjects } from "./listProjects.js";
9
11
  export { solidAdapter } from "./adapter/solidAdapter.js";
@@ -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;AAyBhF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAgChD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACvC,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,cAAc,CAAA;CACxB,GAAG,OAAO,CAAC,aAAa,CAAC,CAqYzB;AAsHD,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;AAwBhF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAgChD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACvC,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,cAAc,CAAA;CACxB,GAAG,OAAO,CAAC,aAAa,CAAC,CA2XzB;AAsHD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAQtE"}
@@ -1,6 +1,6 @@
1
1
  import { resolveModules } from "./resolve-modules/index.js";
2
2
  import { TypeCompiler, ValueErrorType } from "@sinclair/typebox/compiler";
3
- import { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginSaveMessagesError, LoadProjectInvalidArgument, PluginLoadMessagesError, } from "./errors.js";
3
+ import { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginSaveMessagesError, PluginLoadMessagesError, } from "./errors.js";
4
4
  import { createRoot, createSignal, createEffect } from "./reactivity/solid.js";
5
5
  import { createMessagesQuery } from "./createMessagesQuery.js";
6
6
  import { createMessageLintReportsQuery } from "./createMessageLintReportsQuery.js";
@@ -9,7 +9,7 @@ import { tryCatch } from "@inlang/result";
9
9
  import { migrateIfOutdated } from "@inlang/project-settings/migration";
10
10
  import { createNodeishFsWithAbsolutePaths } from "./createNodeishFsWithAbsolutePaths.js";
11
11
  import { normalizePath } from "@lix-js/fs";
12
- import { isAbsolutePath } from "./isAbsolutePath.js";
12
+ import { assertValidProjectPath } from "./validateProjectPath.js";
13
13
  import { maybeMigrateToDirectory } from "./migrations/migrateToDirectory.js";
14
14
  import { stringifyMessage as stringifyMessage } from "./storage/helper.js";
15
15
  import { humanIdHash } from "./storage/human-id/human-readable-id.js";
@@ -43,12 +43,7 @@ export async function loadProject(args) {
43
43
  // the only place where throwing is acceptable because the project
44
44
  // won't even be loaded. do not throw anywhere else. otherwise, apps
45
45
  // can't handle errors gracefully.
46
- if (!isAbsolutePath(args.projectPath)) {
47
- throw new LoadProjectInvalidArgument(`Expected an absolute path but received "${args.projectPath}".`, { argument: "projectPath" });
48
- }
49
- else if (/[^\\/]+\.inlang$/.test(projectPath) === false) {
50
- 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" });
51
- }
46
+ assertValidProjectPath(projectPath);
52
47
  const nodeishFs = createNodeishFsWithAbsolutePaths({
53
48
  projectPath,
54
49
  nodeishFs: args.repo.nodeishFs,
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
2
  import { describe, it, expect, vi } from "vitest";
3
3
  import { loadProject } from "./loadProject.js";
4
- import { LoadProjectInvalidArgument, ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, } from "./errors.js";
4
+ import { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, } from "./errors.js";
5
5
  import { normalizePath } from "@lix-js/fs";
6
6
  import { createMessage } from "./test-utilities/createMessage.js";
7
7
  import { tryCatch } from "@inlang/result";
@@ -144,7 +144,7 @@ it("should throw if a project (path) does not have a name", async () => {
144
144
  repo,
145
145
  _import,
146
146
  }));
147
- expect(project.error).toBeInstanceOf(LoadProjectInvalidArgument);
147
+ expect(project?.error?.message).toMatch('Expected a path ending in "{name}.inlang" but received ');
148
148
  });
149
149
  it("should throw if a project path does not end with .inlang", async () => {
150
150
  const repo = await mockRepo();
@@ -159,7 +159,7 @@ it("should throw if a project path does not end with .inlang", async () => {
159
159
  repo,
160
160
  _import,
161
161
  }));
162
- expect(project.error).toBeInstanceOf(LoadProjectInvalidArgument);
162
+ expect(project.error?.message).toMatch('Expected a path ending in "{name}.inlang" but received ');
163
163
  }
164
164
  });
165
165
  describe("initialization", () => {
@@ -170,7 +170,7 @@ describe("initialization", () => {
170
170
  repo,
171
171
  _import,
172
172
  }));
173
- expect(result.error).toBeInstanceOf(LoadProjectInvalidArgument);
173
+ expect(result.error?.message).toBe('Expected an absolute path but received "relative/path".');
174
174
  expect(result.data).toBeUndefined();
175
175
  });
176
176
  it("should generate projectId on missing projectid", async () => {
@@ -1,7 +1,6 @@
1
1
  import { generateProjectId } from "./maybeCreateFirstProjectId.js";
2
2
  import { it, expect } from "vitest";
3
- import { openRepository } from "@lix-js/client/src/openRepository.ts";
4
- import { mockRepo, createNodeishMemoryFs } from "@lix-js/client";
3
+ import { mockRepo } from "@lix-js/client";
5
4
  import {} from "@lix-js/fs";
6
5
  // eslint-disable-next-line no-restricted-imports -- test
7
6
  import { readFileSync } from "node:fs";
@@ -16,11 +15,9 @@ it("should generate a project id", async () => {
16
15
  expect(projectId).toBe("432d7ef29c510e99d95e2d14ef57a0797a1603859b5a851b7dff7e77161b8c08");
17
16
  });
18
17
  it("should return undefined if repoMeta contains error", async () => {
19
- const repoWithError = await openRepository("https://github.com/inlang/no-exist", {
20
- nodeishFs: createNodeishMemoryFs(),
21
- });
18
+ await repo.nodeishFs.rm("/.git", { recursive: true });
22
19
  const projectId = await generateProjectId({
23
- repo: repoWithError,
20
+ repo: repo,
24
21
  projectPath: "mocked_project_path",
25
22
  });
26
23
  expect(projectId).toBeUndefined();
@@ -0,0 +1,23 @@
1
+ import type { NodeishFilesystem } from "@lix-js/fs";
2
+ /**
3
+ * validate that a project path is absolute and ends with {name}.inlang.
4
+ *
5
+ * @throws if the path is not valid.
6
+ */
7
+ export declare function assertValidProjectPath(projectPath: string): void;
8
+ /**
9
+ * tests whether a path ends with {name}.inlang
10
+ * (does not remove trailing slash)
11
+ */
12
+ export declare function isInlangProjectPath(path: string): boolean;
13
+ /**
14
+ * tests whether a path starts with a forward slash (/) or a windows-style
15
+ * drive letter (C: or D:, etc.) followed by a slash
16
+ */
17
+ export declare function isAbsolutePath(path: string): boolean;
18
+ /**
19
+ * Returns true if the path exists (file or directory), false otherwise.
20
+ *
21
+ */
22
+ export declare function pathExists(filePath: string, nodeishFs: NodeishFilesystem): Promise<boolean>;
23
+ //# sourceMappingURL=validateProjectPath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateProjectPath.d.ts","sourceRoot":"","sources":["../src/validateProjectPath.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,QASzD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,WAE/C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,WAO1C;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,iBAAiB,oBAc9E"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * validate that a project path is absolute and ends with {name}.inlang.
3
+ *
4
+ * @throws if the path is not valid.
5
+ */
6
+ export function assertValidProjectPath(projectPath) {
7
+ if (!isAbsolutePath(projectPath)) {
8
+ throw new Error(`Expected an absolute path but received "${projectPath}".`);
9
+ }
10
+ if (!isInlangProjectPath(projectPath)) {
11
+ throw new Error(`Expected a path ending in "{name}.inlang" but received "${projectPath}".\n\nValid examples: \n- "/path/to/micky-mouse.inlang"\n- "/path/to/green-elephant.inlang\n`);
12
+ }
13
+ }
14
+ /**
15
+ * tests whether a path ends with {name}.inlang
16
+ * (does not remove trailing slash)
17
+ */
18
+ export function isInlangProjectPath(path) {
19
+ return /[^\\/]+\.inlang$/.test(path);
20
+ }
21
+ /**
22
+ * tests whether a path starts with a forward slash (/) or a windows-style
23
+ * drive letter (C: or D:, etc.) followed by a slash
24
+ */
25
+ export function isAbsolutePath(path) {
26
+ return /^\/|^[A-Za-z]:[\\/]/.test(path);
27
+ // OG from sdk/src/isAbsolutePath.ts - TODO: find out where this regex came from
28
+ // const matchPosixAndWindowsAbsolutePaths =
29
+ // /^(?:[A-Za-z]:\\(?:[^\\]+\\)*[^\\]+|[A-Za-z]:\/(?:[^/]+\/)*[^/]+|\/(?:[^/]+\/)*[^/]+)$/
30
+ // return matchPosixAndWindowsAbsolutePaths.test(path)
31
+ }
32
+ /**
33
+ * Returns true if the path exists (file or directory), false otherwise.
34
+ *
35
+ */
36
+ export async function pathExists(filePath, nodeishFs) {
37
+ // from paraglide-js/src/services/file-handling/exists.ts
38
+ // TODO: add fs.exists to @lix-js/fs
39
+ try {
40
+ await nodeishFs.stat(filePath);
41
+ return true;
42
+ }
43
+ catch (error) {
44
+ //@ts-ignore
45
+ if (error.code === "ENOENT") {
46
+ return false;
47
+ }
48
+ else {
49
+ throw new Error(`Failed to check if path exists: ${error}`, { cause: error });
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=validateProjectPath.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateProjectPath.test.d.ts","sourceRoot":"","sources":["../src/validateProjectPath.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,56 @@
1
+ import { assert, describe, expect, it } from "vitest";
2
+ import { assertValidProjectPath, isAbsolutePath, isInlangProjectPath, pathExists, } from "./validateProjectPath.js";
3
+ import { mockRepo } from "@lix-js/client";
4
+ describe("isAbsolutePath", () => {
5
+ it("should correctly identify Unix absolute paths", () => {
6
+ assert.isTrue(isAbsolutePath("/home/user/documents/file.txt"));
7
+ assert.isTrue(isAbsolutePath("/usr/local/bin/script.sh"));
8
+ assert.isFalse(isAbsolutePath("relative/path/to/file.txt"));
9
+ });
10
+ it("should correctly identify Windows absolute paths", () => {
11
+ assert.isTrue(isAbsolutePath("C:\\Users\\User\\Documents\\File.txt"));
12
+ assert.isTrue(isAbsolutePath("C:/Users/user/project.inlang/settings.json"));
13
+ assert.isFalse(isAbsolutePath("Projects\\Project1\\source\\file.txt"));
14
+ });
15
+ it("should handle edge cases", () => {
16
+ assert.isFalse(isAbsolutePath("")); // Empty path should return false
17
+ assert.isFalse(isAbsolutePath("relative/path/../file.txt")); // Relative path with ".." should return false
18
+ assert.isFalse(isAbsolutePath("../relative/path/to/file.txt"));
19
+ assert.isFalse(isAbsolutePath("./relative/path/to/file.txt"));
20
+ });
21
+ });
22
+ describe("isInlangProjectPath", () => {
23
+ it("should correctly identify valid inlang project paths", () => {
24
+ assert.isTrue(isInlangProjectPath("/path/to/orange-mouse.inlang"));
25
+ assert.isFalse(isInlangProjectPath("relative/path/to/file.txt"));
26
+ assert.isFalse(isInlangProjectPath("/path/to/.inlang"));
27
+ assert.isFalse(isInlangProjectPath("/path/to/white-elephant.inlang/"));
28
+ assert.isFalse(isInlangProjectPath("/path/to/blue-elephant.inlang/settings.json"));
29
+ });
30
+ });
31
+ describe("assertValidProjectPath", () => {
32
+ it("should not throw for valid project paths", () => {
33
+ assert.doesNotThrow(() => assertValidProjectPath("/path/to/brown-mouse.inlang"));
34
+ assert.doesNotThrow(() => assertValidProjectPath("/path/to/green-elephant.inlang"));
35
+ });
36
+ it("should throw for invalid project paths", () => {
37
+ assert.throws(() => assertValidProjectPath("relative/path/to/flying-lizard.inlang"));
38
+ assert.throws(() => assertValidProjectPath("/path/to/loud-mouse.inlang/"));
39
+ assert.throws(() => assertValidProjectPath("/path/to/green-elephant.inlang/settings.json"));
40
+ });
41
+ });
42
+ // moar tests in paraglide-js/src/services/file-handling/exists.test.ts
43
+ describe("pathExists", () => {
44
+ it("should work for files", async () => {
45
+ const repo = await mockRepo();
46
+ await repo.nodeishFs.writeFile("/test.txt", "hello");
47
+ expect(await pathExists("/test.txt", repo.nodeishFs)).toBe(true);
48
+ expect(await pathExists("/does-not-exist.txt", repo.nodeishFs)).toBe(false);
49
+ });
50
+ it("should work for directories", async () => {
51
+ const repo = await mockRepo();
52
+ await repo.nodeishFs.mkdir("/test/project.inlang", { recursive: true });
53
+ expect(await pathExists("/test/project.inlang", repo.nodeishFs)).toBe(true);
54
+ expect(await pathExists("/test/white-gorilla.inlang", repo.nodeishFs)).toBe(false);
55
+ });
56
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inlang/sdk",
3
3
  "type": "module",
4
- "version": "0.32.0",
4
+ "version": "0.33.0",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -32,16 +32,16 @@
32
32
  "murmurhash3js": "^3.0.1",
33
33
  "solid-js": "1.6.12",
34
34
  "throttle-debounce": "^5.0.0",
35
- "@inlang/language-tag": "1.5.1",
36
35
  "@inlang/json-types": "1.1.0",
36
+ "@inlang/language-tag": "1.5.1",
37
37
  "@inlang/message": "2.1.0",
38
38
  "@inlang/message-lint-rule": "1.4.5",
39
39
  "@inlang/module": "1.2.9",
40
40
  "@inlang/plugin": "2.4.9",
41
- "@inlang/project-settings": "2.4.0",
42
41
  "@inlang/result": "1.1.0",
42
+ "@inlang/project-settings": "2.4.0",
43
43
  "@inlang/translatable": "1.3.1",
44
- "@lix-js/client": "1.2.0",
44
+ "@lix-js/client": "1.2.1",
45
45
  "@lix-js/fs": "1.0.0"
46
46
  },
47
47
  "devDependencies": {
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { createNewProject } from "./createNewProject.js"
3
+ import { mockRepo } from "@lix-js/client"
4
+ import { defaultProjectSettings } from "./defaultProjectSettings.js"
5
+ import { loadProject } from "./loadProject.js"
6
+ import { createMessage } from "./test-utilities/createMessage.js"
7
+
8
+ function sleep(ms: number) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms))
10
+ }
11
+
12
+ describe("createNewProject", () => {
13
+ it("should throw if a path does not end with .inlang", async () => {
14
+ const repo = await mockRepo()
15
+ const projectPath = "/test/project.inl"
16
+ try {
17
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
18
+ // should not reach this point
19
+ throw new Error("Expected an error")
20
+ } catch (e) {
21
+ expect((e as Error).message).toMatch(
22
+ 'Expected a path ending in "{name}.inlang" but received "/test/project.inl"'
23
+ )
24
+ }
25
+ })
26
+
27
+ it("should throw if projectPath is not an absolute path", async () => {
28
+ const repo = await mockRepo()
29
+ const projectPath = "test/project.inlang"
30
+ try {
31
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
32
+ // should not reach this point
33
+ throw new Error("Expected an error")
34
+ } catch (e) {
35
+ expect((e as Error).message).toMatch(
36
+ 'Expected an absolute path but received "test/project.inlang"'
37
+ )
38
+ }
39
+ })
40
+
41
+ it("should throw if the path already exists", async () => {
42
+ const repo = await mockRepo()
43
+ const projectPath = "/test/project.inlang"
44
+ await repo.nodeishFs.mkdir(projectPath, { recursive: true })
45
+ try {
46
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
47
+ // should not reach this point
48
+ throw new Error("Expected an error")
49
+ } catch (e) {
50
+ expect((e as Error).message).toMatch(
51
+ 'projectPath already exists, received "/test/project.inlang"'
52
+ )
53
+ }
54
+ })
55
+
56
+ it("should create default defaultProjectSettings in projectPath", async () => {
57
+ const repo = await mockRepo()
58
+ const projectPath = "/test/project.inlang"
59
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
60
+ const json = await repo.nodeishFs.readFile(`${projectPath}/settings.json`, {
61
+ encoding: "utf-8",
62
+ })
63
+ const settings = JSON.parse(json)
64
+ expect(settings).toEqual(defaultProjectSettings)
65
+ })
66
+
67
+ it("should create different projectSettings in projectPath", async () => {
68
+ const repo = await mockRepo()
69
+ const projectPath = "/test/project.inlang"
70
+ const projectSettings = { ...defaultProjectSettings, languageTags: ["en", "de", "fr"] }
71
+ await createNewProject({ projectPath, repo, projectSettings })
72
+ const json = await repo.nodeishFs.readFile(`${projectPath}/settings.json`, {
73
+ encoding: "utf-8",
74
+ })
75
+ const settings = JSON.parse(json)
76
+ expect(settings).toEqual(projectSettings)
77
+ expect(settings).not.toEqual(defaultProjectSettings)
78
+ })
79
+
80
+ it("should load the project after creating it", async () => {
81
+ const repo = await mockRepo()
82
+ const projectPath = "/test/project.inlang"
83
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
84
+
85
+ const project = await loadProject({ projectPath, repo })
86
+ expect(project.errors().length).toBe(0)
87
+ })
88
+
89
+ it("should create messages inside the project directory", async () => {
90
+ const repo = await mockRepo()
91
+ const projectPath = "/test/project.inlang"
92
+ await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
93
+ const project = await loadProject({ projectPath, repo })
94
+ expect(project.errors().length).toBe(0)
95
+
96
+ const testMessage = createMessage("test", { en: "test message" })
97
+ project.query.messages.create({ data: testMessage })
98
+ const messages = project.query.messages.getAll()
99
+ expect(messages.length).toBe(1)
100
+ expect(messages[0]).toEqual(testMessage)
101
+
102
+ await sleep(20)
103
+
104
+ const json = await repo.nodeishFs.readFile("/test/messages/en.json", { encoding: "utf-8" })
105
+ const jsonMessages = JSON.parse(json)
106
+ expect(jsonMessages["test"]).toEqual("test message")
107
+ })
108
+ })
@@ -0,0 +1,31 @@
1
+ import type { Repository } from "@lix-js/client"
2
+ import { ProjectSettings } from "@inlang/project-settings"
3
+ import { assertValidProjectPath, pathExists } from "./validateProjectPath.js"
4
+ import { defaultProjectSettings } from "./defaultProjectSettings.js"
5
+
6
+ /**
7
+ * Creates a new project in the given directory.
8
+ * The directory must be an absolute path, must not exist, and must end with {name}.inlang
9
+ * Uses defaultProjectSettings unless projectSettings are provided.
10
+ *
11
+ * @param projectPath - Absolute path to the [name].inlang directory
12
+ * @param repo - An instance of a lix repo as returned by `openRepository`
13
+ * @param projectSettings - Optional project settings to use for the new project.
14
+ */
15
+ export async function createNewProject(args: {
16
+ projectPath: string
17
+ repo: Repository
18
+ projectSettings: ProjectSettings
19
+ }): Promise<void> {
20
+ assertValidProjectPath(args.projectPath)
21
+
22
+ const nodeishFs = args.repo.nodeishFs
23
+ if (await pathExists(args.projectPath, nodeishFs)) {
24
+ throw new Error(`projectPath already exists, received "${args.projectPath}"`)
25
+ }
26
+ await nodeishFs.mkdir(args.projectPath, { recursive: true })
27
+
28
+ const settingsText = JSON.stringify(args.projectSettings ?? defaultProjectSettings, undefined, 2)
29
+
30
+ await nodeishFs.writeFile(`${args.projectPath}/settings.json`, settingsText)
31
+ }
@@ -1,5 +1,5 @@
1
1
  import { normalizePath, type NodeishFilesystem } from "@lix-js/fs"
2
- import { isAbsolutePath } from "./isAbsolutePath.js"
2
+ import { isAbsolutePath } from "./validateProjectPath.js"
3
3
 
4
4
  /**
5
5
  * Wraps the nodeish filesystem subset with a function that intercepts paths
@@ -0,0 +1,27 @@
1
+ import type { ProjectSettings } from "@inlang/project-settings"
2
+
3
+ /**
4
+ * Default project settings for createNewProject
5
+ * from paraglide-js/src/cli/commands/init/defaults.ts
6
+ */
7
+ export const defaultProjectSettings = {
8
+ $schema: "https://inlang.com/schema/project-settings",
9
+ sourceLanguageTag: "en",
10
+ languageTags: ["en"],
11
+ modules: [
12
+ // for instant gratification, we're adding common rules
13
+ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
14
+ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js",
15
+ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js",
16
+ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@latest/dist/index.js",
17
+
18
+ // default to the message format plugin because it supports all features
19
+ "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js",
20
+
21
+ // the m function matcher should be installed by default in case Sherlock (VS Code extension) is adopted
22
+ "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js",
23
+ ],
24
+ "plugin.inlang.messageFormat": {
25
+ pathPattern: "./messages/{languageTag}.json",
26
+ },
27
+ } satisfies ProjectSettings
package/src/index.ts CHANGED
@@ -12,6 +12,8 @@ export type {
12
12
  Subscribable,
13
13
  } from "./api.js"
14
14
  export { type ImportFunction, createImport } from "./resolve-modules/index.js"
15
+ export { createNewProject } from "./createNewProject.js"
16
+ export { defaultProjectSettings } from "./defaultProjectSettings.js"
15
17
  export { loadProject } from "./loadProject.js"
16
18
  export { listProjects } from "./listProjects.js"
17
19
  export { solidAdapter, type InlangProjectWithSolidAdapter } from "./adapter/solidAdapter.js"
@@ -11,7 +11,6 @@ import type {
11
11
  import type { ImportFunction } from "./resolve-modules/index.js"
12
12
  import type { InlangModule } from "@inlang/module"
13
13
  import {
14
- LoadProjectInvalidArgument,
15
14
  ProjectSettingsFileJSONSyntaxError,
16
15
  ProjectSettingsFileNotFoundError,
17
16
  ProjectSettingsInvalidError,
@@ -175,7 +174,7 @@ it("should throw if a project (path) does not have a name", async () => {
175
174
  _import,
176
175
  })
177
176
  )
178
- expect(project.error).toBeInstanceOf(LoadProjectInvalidArgument)
177
+ expect(project?.error?.message).toMatch('Expected a path ending in "{name}.inlang" but received ')
179
178
  })
180
179
 
181
180
  it("should throw if a project path does not end with .inlang", async () => {
@@ -195,7 +194,9 @@ it("should throw if a project path does not end with .inlang", async () => {
195
194
  _import,
196
195
  })
197
196
  )
198
- expect(project.error).toBeInstanceOf(LoadProjectInvalidArgument)
197
+ expect(project.error?.message).toMatch(
198
+ 'Expected a path ending in "{name}.inlang" but received '
199
+ )
199
200
  }
200
201
  })
201
202
 
@@ -210,7 +211,7 @@ describe("initialization", () => {
210
211
  _import,
211
212
  })
212
213
  )
213
- expect(result.error).toBeInstanceOf(LoadProjectInvalidArgument)
214
+ expect(result.error?.message).toBe('Expected an absolute path but received "relative/path".')
214
215
  expect(result.data).toBeUndefined()
215
216
  })
216
217
 
@@ -12,7 +12,6 @@ import {
12
12
  ProjectSettingsFileNotFoundError,
13
13
  ProjectSettingsInvalidError,
14
14
  PluginSaveMessagesError,
15
- LoadProjectInvalidArgument,
16
15
  PluginLoadMessagesError,
17
16
  } from "./errors.js"
18
17
  import { createRoot, createSignal, createEffect } from "./reactivity/solid.js"
@@ -23,7 +22,7 @@ import { tryCatch, type Result } from "@inlang/result"
23
22
  import { migrateIfOutdated } from "@inlang/project-settings/migration"
24
23
  import { createNodeishFsWithAbsolutePaths } from "./createNodeishFsWithAbsolutePaths.js"
25
24
  import { normalizePath, type NodeishFilesystem } from "@lix-js/fs"
26
- import { isAbsolutePath } from "./isAbsolutePath.js"
25
+ import { assertValidProjectPath } from "./validateProjectPath.js"
27
26
  import { maybeMigrateToDirectory } from "./migrations/migrateToDirectory.js"
28
27
 
29
28
  import { stringifyMessage as stringifyMessage } from "./storage/helper.js"
@@ -93,17 +92,7 @@ export async function loadProject(args: {
93
92
  // won't even be loaded. do not throw anywhere else. otherwise, apps
94
93
  // can't handle errors gracefully.
95
94
 
96
- if (!isAbsolutePath(args.projectPath)) {
97
- throw new LoadProjectInvalidArgument(
98
- `Expected an absolute path but received "${args.projectPath}".`,
99
- { argument: "projectPath" }
100
- )
101
- } else if (/[^\\/]+\.inlang$/.test(projectPath) === false) {
102
- throw new LoadProjectInvalidArgument(
103
- `Expected a path ending in "{name}.inlang" but received "${projectPath}".\n\nValid examples: \n- "/path/to/micky-mouse.inlang"\n- "/path/to/green-elephant.inlang\n`,
104
- { argument: "projectPath" }
105
- )
106
- }
95
+ assertValidProjectPath(projectPath)
107
96
 
108
97
  const nodeishFs = createNodeishFsWithAbsolutePaths({
109
98
  projectPath,
@@ -1,7 +1,6 @@
1
1
  import { generateProjectId } from "./maybeCreateFirstProjectId.js"
2
2
  import { it, expect } from "vitest"
3
- import { openRepository } from "@lix-js/client/src/openRepository.ts"
4
- import { mockRepo, createNodeishMemoryFs } from "@lix-js/client"
3
+ import { mockRepo } from "@lix-js/client"
5
4
  import { type Snapshot } from "@lix-js/fs"
6
5
  // eslint-disable-next-line no-restricted-imports -- test
7
6
  import { readFileSync } from "node:fs"
@@ -22,13 +21,11 @@ it("should generate a project id", async () => {
22
21
  })
23
22
 
24
23
  it("should return undefined if repoMeta contains error", async () => {
25
- const repoWithError = await openRepository("https://github.com/inlang/no-exist", {
26
- nodeishFs: createNodeishMemoryFs(),
27
- })
24
+ await repo.nodeishFs.rm("/.git", { recursive: true })
28
25
 
29
26
  const projectId = await generateProjectId({
30
- repo: repoWithError,
27
+ repo: repo,
31
28
  projectPath: "mocked_project_path",
32
29
  })
33
30
  expect(projectId).toBeUndefined()
34
- })
31
+ })
@@ -0,0 +1,68 @@
1
+ import { assert, describe, expect, it } from "vitest"
2
+ import {
3
+ assertValidProjectPath,
4
+ isAbsolutePath,
5
+ isInlangProjectPath,
6
+ pathExists,
7
+ } from "./validateProjectPath.js"
8
+ import { mockRepo } from "@lix-js/client"
9
+
10
+ describe("isAbsolutePath", () => {
11
+ it("should correctly identify Unix absolute paths", () => {
12
+ assert.isTrue(isAbsolutePath("/home/user/documents/file.txt"))
13
+ assert.isTrue(isAbsolutePath("/usr/local/bin/script.sh"))
14
+ assert.isFalse(isAbsolutePath("relative/path/to/file.txt"))
15
+ })
16
+
17
+ it("should correctly identify Windows absolute paths", () => {
18
+ assert.isTrue(isAbsolutePath("C:\\Users\\User\\Documents\\File.txt"))
19
+ assert.isTrue(isAbsolutePath("C:/Users/user/project.inlang/settings.json"))
20
+ assert.isFalse(isAbsolutePath("Projects\\Project1\\source\\file.txt"))
21
+ })
22
+
23
+ it("should handle edge cases", () => {
24
+ assert.isFalse(isAbsolutePath("")) // Empty path should return false
25
+ assert.isFalse(isAbsolutePath("relative/path/../file.txt")) // Relative path with ".." should return false
26
+ assert.isFalse(isAbsolutePath("../relative/path/to/file.txt"))
27
+ assert.isFalse(isAbsolutePath("./relative/path/to/file.txt"))
28
+ })
29
+ })
30
+
31
+ describe("isInlangProjectPath", () => {
32
+ it("should correctly identify valid inlang project paths", () => {
33
+ assert.isTrue(isInlangProjectPath("/path/to/orange-mouse.inlang"))
34
+ assert.isFalse(isInlangProjectPath("relative/path/to/file.txt"))
35
+ assert.isFalse(isInlangProjectPath("/path/to/.inlang"))
36
+ assert.isFalse(isInlangProjectPath("/path/to/white-elephant.inlang/"))
37
+ assert.isFalse(isInlangProjectPath("/path/to/blue-elephant.inlang/settings.json"))
38
+ })
39
+ })
40
+
41
+ describe("assertValidProjectPath", () => {
42
+ it("should not throw for valid project paths", () => {
43
+ assert.doesNotThrow(() => assertValidProjectPath("/path/to/brown-mouse.inlang"))
44
+ assert.doesNotThrow(() => assertValidProjectPath("/path/to/green-elephant.inlang"))
45
+ })
46
+
47
+ it("should throw for invalid project paths", () => {
48
+ assert.throws(() => assertValidProjectPath("relative/path/to/flying-lizard.inlang"))
49
+ assert.throws(() => assertValidProjectPath("/path/to/loud-mouse.inlang/"))
50
+ assert.throws(() => assertValidProjectPath("/path/to/green-elephant.inlang/settings.json"))
51
+ })
52
+ })
53
+
54
+ // moar tests in paraglide-js/src/services/file-handling/exists.test.ts
55
+ describe("pathExists", () => {
56
+ it("should work for files", async () => {
57
+ const repo = await mockRepo()
58
+ await repo.nodeishFs.writeFile("/test.txt", "hello")
59
+ expect(await pathExists("/test.txt", repo.nodeishFs)).toBe(true)
60
+ expect(await pathExists("/does-not-exist.txt", repo.nodeishFs)).toBe(false)
61
+ })
62
+ it("should work for directories", async () => {
63
+ const repo = await mockRepo()
64
+ await repo.nodeishFs.mkdir("/test/project.inlang", { recursive: true })
65
+ expect(await pathExists("/test/project.inlang", repo.nodeishFs)).toBe(true)
66
+ expect(await pathExists("/test/white-gorilla.inlang", repo.nodeishFs)).toBe(false)
67
+ })
68
+ })
@@ -0,0 +1,58 @@
1
+ import type { NodeishFilesystem } from "@lix-js/fs"
2
+
3
+ /**
4
+ * validate that a project path is absolute and ends with {name}.inlang.
5
+ *
6
+ * @throws if the path is not valid.
7
+ */
8
+ export function assertValidProjectPath(projectPath: string) {
9
+ if (!isAbsolutePath(projectPath)) {
10
+ throw new Error(`Expected an absolute path but received "${projectPath}".`)
11
+ }
12
+ if (!isInlangProjectPath(projectPath)) {
13
+ throw new Error(
14
+ `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`
15
+ )
16
+ }
17
+ }
18
+
19
+ /**
20
+ * tests whether a path ends with {name}.inlang
21
+ * (does not remove trailing slash)
22
+ */
23
+ export function isInlangProjectPath(path: string) {
24
+ return /[^\\/]+\.inlang$/.test(path)
25
+ }
26
+
27
+ /**
28
+ * tests whether a path starts with a forward slash (/) or a windows-style
29
+ * drive letter (C: or D:, etc.) followed by a slash
30
+ */
31
+ export function isAbsolutePath(path: string) {
32
+ return /^\/|^[A-Za-z]:[\\/]/.test(path)
33
+
34
+ // OG from sdk/src/isAbsolutePath.ts - TODO: find out where this regex came from
35
+ // const matchPosixAndWindowsAbsolutePaths =
36
+ // /^(?:[A-Za-z]:\\(?:[^\\]+\\)*[^\\]+|[A-Za-z]:\/(?:[^/]+\/)*[^/]+|\/(?:[^/]+\/)*[^/]+)$/
37
+ // return matchPosixAndWindowsAbsolutePaths.test(path)
38
+ }
39
+
40
+ /**
41
+ * Returns true if the path exists (file or directory), false otherwise.
42
+ *
43
+ */
44
+ export async function pathExists(filePath: string, nodeishFs: NodeishFilesystem) {
45
+ // from paraglide-js/src/services/file-handling/exists.ts
46
+ // TODO: add fs.exists to @lix-js/fs
47
+ try {
48
+ await nodeishFs.stat(filePath)
49
+ return true
50
+ } catch (error) {
51
+ //@ts-ignore
52
+ if (error.code === "ENOENT") {
53
+ return false
54
+ } else {
55
+ throw new Error(`Failed to check if path exists: ${error}`, { cause: error })
56
+ }
57
+ }
58
+ }
@@ -1,2 +0,0 @@
1
- export declare const isAbsolutePath: (path: string) => boolean;
2
- //# sourceMappingURL=isAbsolutePath.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"isAbsolutePath.d.ts","sourceRoot":"","sources":["../src/isAbsolutePath.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,SAAU,MAAM,YAI1C,CAAA"}
@@ -1,4 +0,0 @@
1
- export const isAbsolutePath = (path) => {
2
- const matchPosixAndWindowsAbsolutePaths = /^(?:[A-Za-z]:\\(?:[^\\]+\\)*[^\\]+|[A-Za-z]:\/(?:[^/]+\/)*[^/]+|\/(?:[^/]+\/)*[^/]+)$/;
3
- return matchPosixAndWindowsAbsolutePaths.test(path);
4
- };
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=isAbsolutePath.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"isAbsolutePath.test.d.ts","sourceRoot":"","sources":["../src/isAbsolutePath.test.ts"],"names":[],"mappings":""}
@@ -1,20 +0,0 @@
1
- import { assert, describe, it } from "vitest";
2
- import { isAbsolutePath } from "./isAbsolutePath.js";
3
- describe("isAbsolutePath", () => {
4
- it("should correctly identify Unix absolute paths", () => {
5
- assert.isTrue(isAbsolutePath("/home/user/documents/file.txt"));
6
- assert.isTrue(isAbsolutePath("/usr/local/bin/script.sh"));
7
- assert.isFalse(isAbsolutePath("relative/path/to/file.txt"));
8
- });
9
- it("should correctly identify Windows absolute paths", () => {
10
- assert.isTrue(isAbsolutePath("C:\\Users\\User\\Documents\\File.txt"));
11
- assert.isTrue(isAbsolutePath("C:/Users/user/project.inlang/settings.json"));
12
- assert.isFalse(isAbsolutePath("Projects\\Project1\\source\\file.txt"));
13
- });
14
- it("should handle edge cases", () => {
15
- assert.isFalse(isAbsolutePath("")); // Empty path should return false
16
- assert.isFalse(isAbsolutePath("relative/path/../file.txt")); // Relative path with ".." should return false
17
- assert.isFalse(isAbsolutePath("../relative/path/to/file.txt"));
18
- assert.isFalse(isAbsolutePath("./relative/path/to/file.txt"));
19
- });
20
- });
@@ -1,23 +0,0 @@
1
- import { assert, describe, it } from "vitest"
2
- import { isAbsolutePath } from "./isAbsolutePath.js"
3
-
4
- describe("isAbsolutePath", () => {
5
- it("should correctly identify Unix absolute paths", () => {
6
- assert.isTrue(isAbsolutePath("/home/user/documents/file.txt"))
7
- assert.isTrue(isAbsolutePath("/usr/local/bin/script.sh"))
8
- assert.isFalse(isAbsolutePath("relative/path/to/file.txt"))
9
- })
10
-
11
- it("should correctly identify Windows absolute paths", () => {
12
- assert.isTrue(isAbsolutePath("C:\\Users\\User\\Documents\\File.txt"))
13
- assert.isTrue(isAbsolutePath("C:/Users/user/project.inlang/settings.json"))
14
- assert.isFalse(isAbsolutePath("Projects\\Project1\\source\\file.txt"))
15
- })
16
-
17
- it("should handle edge cases", () => {
18
- assert.isFalse(isAbsolutePath("")) // Empty path should return false
19
- assert.isFalse(isAbsolutePath("relative/path/../file.txt")) // Relative path with ".." should return false
20
- assert.isFalse(isAbsolutePath("../relative/path/to/file.txt"))
21
- assert.isFalse(isAbsolutePath("./relative/path/to/file.txt"))
22
- })
23
- })
@@ -1,5 +0,0 @@
1
- export const isAbsolutePath = (path: string) => {
2
- const matchPosixAndWindowsAbsolutePaths =
3
- /^(?:[A-Za-z]:\\(?:[^\\]+\\)*[^\\]+|[A-Za-z]:\/(?:[^/]+\/)*[^/]+|\/(?:[^/]+\/)*[^/]+)$/
4
- return matchPosixAndWindowsAbsolutePaths.test(path)
5
- }