@inlang/sdk 0.22.0 → 0.23.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 (44) hide show
  1. package/README.md +2 -2
  2. package/dist/createNodeishFsWithWatcher.js +1 -1
  3. package/dist/env-variables/index.d.ts +4 -0
  4. package/dist/env-variables/index.d.ts.map +1 -0
  5. package/dist/env-variables/index.js +3 -0
  6. package/dist/listProjects.d.ts.map +1 -1
  7. package/dist/listProjects.js +15 -9
  8. package/dist/listProjects.test.js +14 -1
  9. package/dist/loadProject.d.ts +28 -9
  10. package/dist/loadProject.d.ts.map +1 -1
  11. package/dist/loadProject.js +49 -50
  12. package/dist/loadProject.test.js +8 -5
  13. package/dist/migrations/maybeCreateFirstProjectId.d.ts +16 -0
  14. package/dist/migrations/maybeCreateFirstProjectId.d.ts.map +1 -0
  15. package/dist/migrations/maybeCreateFirstProjectId.js +38 -0
  16. package/dist/migrations/maybeCreateFirstProjectId.test.d.ts +2 -0
  17. package/dist/migrations/maybeCreateFirstProjectId.test.d.ts.map +1 -0
  18. package/dist/migrations/maybeCreateFirstProjectId.test.js +27 -0
  19. package/dist/migrations/migrateToDirectory.d.ts +1 -1
  20. package/dist/migrations/migrateToDirectory.js +3 -3
  21. package/dist/telemetry/capture.d.ts +21 -0
  22. package/dist/telemetry/capture.d.ts.map +1 -0
  23. package/dist/telemetry/capture.js +39 -0
  24. package/package.json +13 -12
  25. package/src/createNodeishFsWithWatcher.ts +1 -1
  26. package/src/env-variables/.prettierignore +1 -0
  27. package/src/env-variables/createIndexFile.js +28 -0
  28. package/src/env-variables/index.d.ts +13 -0
  29. package/src/listProjects.test.ts +21 -2
  30. package/src/listProjects.ts +14 -7
  31. package/src/loadProject.test.ts +12 -5
  32. package/src/loadProject.ts +91 -48
  33. package/src/migrations/maybeCreateFirstProjectId.test.ts +34 -0
  34. package/src/migrations/maybeCreateFirstProjectId.ts +44 -0
  35. package/src/migrations/migrateToDirectory.ts +3 -3
  36. package/src/telemetry/capture.ts +49 -0
  37. package/dist/generateProjectId.d.ts +0 -3
  38. package/dist/generateProjectId.d.ts.map +0 -1
  39. package/dist/generateProjectId.js +0 -11
  40. package/dist/generateProjectId.test.d.ts +0 -2
  41. package/dist/generateProjectId.test.d.ts.map +0 -1
  42. package/dist/generateProjectId.test.js +0 -18
  43. package/src/generateProjectId.test.ts +0 -22
  44. package/src/generateProjectId.ts +0 -14
package/README.md CHANGED
@@ -2,12 +2,12 @@ Developer-first localization infrastructure that is built on git. Your git repos
2
2
 
3
3
  <div>
4
4
  <p align="center">
5
- <img width="300" src="https://cdn.jsdelivr.net/gh/inlang/monorepo/inlang/assets/logo-white-background.png"/>
5
+ <img width="300" src="https://cdn.jsdelivr.net/gh/opral/monorepo/inlang/assets/logo-white-background.png"/>
6
6
  </p>
7
7
  <h4 align="center">
8
8
  <!-- <a href="https://inlang.com/documentation" target="_blank">Get Started</a>
9
9
  · -->
10
- <a href="https://github.com/inlang/monorepo/discussions" target="_blank">Discussions</a> · <a href="https://twitter.com/inlangHQ" target="_blank">Twitter</a>
10
+ <a href="https://github.com/opral/monorepo/discussions" target="_blank">Discussions</a> · <a href="https://twitter.com/inlangHQ" target="_blank">Twitter</a>
11
11
  </h4>
12
12
  </div>
13
13
 
@@ -24,7 +24,7 @@ export const createNodeishFsWithWatcher = (args) => {
24
24
  catch (err) {
25
25
  if (err.name === "AbortError")
26
26
  return;
27
- // https://github.com/inlang/monorepo/issues/1647
27
+ // https://github.com/opral/monorepo/issues/1647
28
28
  // the file does not exist (yet)
29
29
  // this is not testable beacause the fs.watch api differs
30
30
  // from node and lix. lenghty
@@ -0,0 +1,4 @@
1
+ export declare const ENV_VARIABLES: {
2
+ PUBLIC_POSTHOG_TOKEN: string;
3
+ };
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/env-variables/index.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,aAAa;;CAEzB,CAAA"}
@@ -0,0 +1,3 @@
1
+ export const ENV_VARIABLES = {
2
+ PUBLIC_POSTHOG_TOKEN: "phc_m5yJZCxjOGxF8CJvP5sQ3H0d76xpnLrsmiZHduT4jDz",
3
+ };
@@ -1 +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"}
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,CAwCxC,CAAA"}
@@ -9,17 +9,23 @@ export const listProjects = async (nodeishFs, from) => {
9
9
  const files = await nodeishFs.readdir(path);
10
10
  for (const file of files) {
11
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);
12
+ try {
13
+ const stats = await nodeishFs.stat(filePath);
14
+ if (stats.isDirectory()) {
15
+ if (file === "node_modules") {
16
+ continue;
17
+ }
18
+ if (file.endsWith(".inlang")) {
19
+ projects.push({ projectPath: filePath });
20
+ }
21
+ else {
22
+ await searchDir(filePath, depth + 1);
23
+ }
21
24
  }
22
25
  }
26
+ catch {
27
+ continue;
28
+ }
23
29
  }
24
30
  }
25
31
  await searchDir(from, 0);
@@ -1,6 +1,9 @@
1
- import { assert, describe, it } from "vitest";
1
+ import { assert, describe, it, expect } from "vitest";
2
2
  import { listProjects } from "./listProjects.js";
3
3
  import { createNodeishMemoryFs } from "@lix-js/fs";
4
+ import { mockRepo } from "@lix-js/client";
5
+ // eslint-disable-next-line no-restricted-imports -- test
6
+ import { readFileSync } from "node:fs";
4
7
  const settings = {
5
8
  sourceLanguageTag: "en",
6
9
  languageTags: ["en"],
@@ -43,6 +46,16 @@ describe("listProjects", () => {
43
46
  assert(projects.length === 0);
44
47
  });
45
48
  });
49
+ it("should not crash on broken symlinks as cal.com has", async () => {
50
+ const ciTestRepo = JSON.parse(readFileSync("./mocks/ci-test-repo-no-shallow.json", { encoding: "utf-8" }));
51
+ const repo = await mockRepo({ fromSnapshot: ciTestRepo });
52
+ repo.checkout({ branch: "test-symlink" });
53
+ const link = await repo.nodeishFs.readlink("test-symlink-not-existing-target");
54
+ expect(link).toBe("/test-symlink-not-existing-target//.././no-exist");
55
+ await listProjects(repo.nodeishFs, "/").then((projects) => {
56
+ assert(projects.length === 1);
57
+ });
58
+ });
46
59
  it("should also find files inside of a dir that ends with *.inlang", async () => {
47
60
  const fs = createNodeishMemoryFs();
48
61
  await fs.mkdir("/user/dir1/go.inlang", { recursive: true });
@@ -1,23 +1,42 @@
1
1
  import type { InlangProject, Subscribable } from "./api.js";
2
2
  import { type ImportFunction } from "./resolve-modules/index.js";
3
- import { type NodeishFilesystem } from "@lix-js/fs";
4
3
  import type { Repository } from "@lix-js/client";
5
4
  /**
6
5
  * Creates an inlang instance.
7
6
  *
8
7
  * @param projectPath - Absolute path to the inlang settings file.
9
- * @param nodeishFs - Filesystem that implements the NodeishFilesystemSubset interface.
8
+ * @param @deprecated nodeishFs - Filesystem that implements the NodeishFilesystemSubset interface.
10
9
  * @param _import - Use `_import` to pass a custom import function for testing,
11
10
  * and supporting legacy resolvedModules such as CJS.
12
- * @param _capture - Use `_capture` to capture events for analytics.
13
11
  *
14
12
  */
15
- export declare const loadProject: (args: {
13
+ export declare function loadProject(args: {
16
14
  projectPath: string;
17
- repo?: Repository | undefined;
18
- nodeishFs: NodeishFilesystem;
19
- _import?: ImportFunction | undefined;
20
- _capture?: ((id: string, props: Record<string, unknown>) => void) | undefined;
21
- }) => Promise<InlangProject>;
15
+ nodeishFs: Repository["nodeishFs"];
16
+ /**
17
+ * The app id is used to identify the app that is using the SDK.
18
+ *
19
+ * We use the app id to group events in telemetry to answer questions
20
+ * like "Which apps causes these errors?" or "Which apps are used more than others?".
21
+ *
22
+ * @example
23
+ * appId: "app.inlang.badge"
24
+ */
25
+ appId?: string;
26
+ _import?: ImportFunction;
27
+ }): Promise<InlangProject>;
28
+ /**
29
+ * @param projectPath - Absolute path to the inlang settings file.
30
+ * @param repo - An instance of a lix repo as returned by `openRepository`.
31
+ * @param _import - Use `_import` to pass a custom import function for testing,
32
+ * and supporting legacy resolvedModules such as CJS.
33
+ *
34
+ */
35
+ export declare function loadProject(args: {
36
+ projectPath: string;
37
+ repo: Repository;
38
+ appId?: string;
39
+ _import?: ImportFunction;
40
+ }): Promise<InlangProject>;
22
41
  export declare function createSubscribable<T>(signal: () => T): Subscribable<T>;
23
42
  //# sourceMappingURL=loadProject.d.ts.map
@@ -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;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"}
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;AAuBhF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAKhD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACvC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,UAAU,CAAC,WAAW,CAAC,CAAA;IAClC;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,cAAc,CAAA;CACxB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;AAE1B;;;;;;GAMG;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,CAAA;AA+Y1B,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAQtE"}
@@ -13,22 +13,11 @@ 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
+ import { maybeCreateFirstProjectId } from "./migrations/maybeCreateFirstProjectId.js";
17
+ import { capture } from "./telemetry/capture.js";
17
18
  const settingsCompiler = TypeCompiler.Compile(ProjectSettings);
18
- /**
19
- * Creates an inlang instance.
20
- *
21
- * @param projectPath - Absolute path to the inlang settings file.
22
- * @param nodeishFs - Filesystem that implements the NodeishFilesystemSubset interface.
23
- * @param _import - Use `_import` to pass a custom import function for testing,
24
- * and supporting legacy resolvedModules such as CJS.
25
- * @param _capture - Use `_capture` to capture events for analytics.
26
- *
27
- */
28
- export const loadProject = async (args) => {
19
+ export async function loadProject(args) {
29
20
  const projectPath = normalizePath(args.projectPath);
30
- // -- migrate if outdated ------------------------------------------------
31
- await maybeMigrateToDirectory({ nodeishFs: args.nodeishFs, projectPath });
32
21
  // -- validation --------------------------------------------------------
33
22
  // the only place where throwing is acceptable because the project
34
23
  // won't even be loaded. do not throw anywhere else. otherwise, apps
@@ -39,34 +28,31 @@ export const loadProject = async (args) => {
39
28
  else if (/[^\\/]+\.inlang$/.test(projectPath) === false) {
40
29
  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" });
41
30
  }
31
+ let fs;
32
+ if (args.nodeishFs) {
33
+ // TODO: deprecate
34
+ fs = args.nodeishFs;
35
+ }
36
+ else if (args.repo) {
37
+ fs = args.repo.nodeishFs;
38
+ }
39
+ else {
40
+ throw new LoadProjectInvalidArgument(`Repo missing from arguments.`, { argument: "repo" });
41
+ }
42
+ const nodeishFs = createNodeishFsWithAbsolutePaths({
43
+ projectPath,
44
+ nodeishFs: fs,
45
+ });
46
+ // -- migratations ------------------------------------------------
47
+ await maybeMigrateToDirectory({ nodeishFs: fs, projectPath });
48
+ await maybeCreateFirstProjectId({ projectPath, repo: args.repo });
42
49
  // -- load project ------------------------------------------------------
43
- let idError;
44
50
  return await createRoot(async () => {
51
+ // TODO remove tryCatch after https://github.com/opral/monorepo/issues/2013
52
+ // - a repo will always be present
53
+ // - if a repo is present, the project id will always be present
54
+ const { data: projectId } = await tryCatch(() => fs.readFile(args.projectPath + "/project_id", { encoding: "utf-8" }));
45
55
  const [initialized, markInitAsComplete, markInitAsFailed] = createAwaitable();
46
- const nodeishFs = createNodeishFsWithAbsolutePaths({
47
- projectPath,
48
- nodeishFs: args.nodeishFs,
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
- }
70
56
  // -- settings ------------------------------------------------------------
71
57
  const [settings, _setSettings] = createSignal();
72
58
  createEffect(() => {
@@ -77,12 +63,7 @@ export const loadProject = async (args) => {
77
63
  // })
78
64
  // }
79
65
  loadSettings({ settingsFilePath: projectPath + "/settings.json", nodeishFs })
80
- .then((settings) => {
81
- setSettings(settings);
82
- // rename settings to get a convenient access to the data in Posthog
83
- const project_settings = settings;
84
- args._capture?.("SDK used settings", { project_settings, group: projectId });
85
- })
66
+ .then((settings) => setSettings(settings))
86
67
  .catch((err) => {
87
68
  markInitAsFailed(err);
88
69
  });
@@ -158,7 +139,7 @@ export const loadProject = async (args) => {
158
139
  description: rule.description,
159
140
  module: resolvedModules()?.meta.find((m) => m.id.includes(rule.id))?.module ??
160
141
  "Unknown module. You stumbled on a bug in inlang's source code. Please open an issue.",
161
- // default to warning, see https://github.com/inlang/monorepo/issues/1254
142
+ // default to warning, see https://github.com/opral/monorepo/issues/1254
162
143
  level: settingsValue["messageLintRuleLevels"]?.[rule.id] ?? "warning",
163
144
  }));
164
145
  };
@@ -203,6 +184,27 @@ export const loadProject = async (args) => {
203
184
  createEffect(() => {
204
185
  debouncedSave(messagesQuery.getAll());
205
186
  });
187
+ /**
188
+ * Utility to escape reactive tracking and avoid multiple calls to
189
+ * the capture event.
190
+ *
191
+ * Should be addressed with https://github.com/opral/monorepo/issues/1772
192
+ */
193
+ let projectLoadedCapturedAlready = false;
194
+ if (projectId && projectLoadedCapturedAlready === false) {
195
+ projectLoadedCapturedAlready = true;
196
+ // TODO ensure that capture is "awaited" without blocking the the app from starting
197
+ await capture("SDK loaded project", {
198
+ projectId,
199
+ properties: {
200
+ appId: args.appId,
201
+ settings: settings(),
202
+ installedPluginIds: installedPlugins().map((p) => p.id),
203
+ installedMessageLintRuleIds: installedMessageLintRules().map((r) => r.id),
204
+ numberOfMessages: messagesQuery.includedMessageIds().length,
205
+ },
206
+ });
207
+ }
206
208
  return {
207
209
  installed: {
208
210
  plugins: createSubscribable(() => installedPlugins()),
@@ -210,10 +212,7 @@ export const loadProject = async (args) => {
210
212
  },
211
213
  errors: createSubscribable(() => [
212
214
  ...(initializeError ? [initializeError] : []),
213
- ...(idError ? [idError] : []),
214
215
  ...(resolvedModules() ? resolvedModules().errors : []),
215
- // have a query error exposed
216
- //...(lintErrors() ?? []),
217
216
  ]),
218
217
  settings: createSubscribable(() => settings()),
219
218
  setSettings,
@@ -224,7 +223,7 @@ export const loadProject = async (args) => {
224
223
  },
225
224
  };
226
225
  });
227
- };
226
+ }
228
227
  //const x = {} as InlangProject
229
228
  // ------------------------------------------------------------------------------------------------
230
229
  const loadSettings = async (args) => {
@@ -6,6 +6,9 @@ import { createNodeishMemoryFs, normalizePath } from "@lix-js/fs";
6
6
  import { createMessage } from "./test-utilities/createMessage.js";
7
7
  import { tryCatch } from "@inlang/result";
8
8
  import { mockRepo } from "@lix-js/client";
9
+ import {} from "@lix-js/fs";
10
+ // eslint-disable-next-line no-restricted-imports -- test
11
+ import { readFileSync } from "node:fs";
9
12
  // ------------------------------------------------------------------------------------------------
10
13
  const getValue = (subscribable) => {
11
14
  let value;
@@ -124,7 +127,8 @@ describe("initialization", () => {
124
127
  expect(result.data).toBeUndefined();
125
128
  });
126
129
  it("should generate projectId on missing projectid", async () => {
127
- const repo = await mockRepo();
130
+ const ciTestRepo = JSON.parse(readFileSync("./mocks/ci-test-repo-no-shallow.json", { encoding: "utf-8" }));
131
+ const repo = await mockRepo({ fromSnapshot: ciTestRepo });
128
132
  const existing = await repo.nodeishFs
129
133
  .readFile("/project.inlang/project_id", {
130
134
  encoding: "utf-8",
@@ -136,7 +140,6 @@ describe("initialization", () => {
136
140
  expect(existing.error.code).toBe("ENOENT");
137
141
  const result = await tryCatch(() => loadProject({
138
142
  projectPath: "/project.inlang",
139
- nodeishFs: repo.nodeishFs,
140
143
  repo,
141
144
  _import,
142
145
  }));
@@ -147,16 +150,16 @@ describe("initialization", () => {
147
150
  .catch((error) => {
148
151
  return { error };
149
152
  });
150
- expect(newId).toBe("7cd6c2b7cf12febf99496408917123fdfe158b6bc442914f5fb42aa74346bd50");
153
+ expect(newId).toBe("e8c61726bc2f437ec6a260abb632b3c59195059b60031e648b5afbafa7f3d79a");
151
154
  expect(result.error).toBeUndefined();
152
155
  expect(result.data).toBeDefined();
153
156
  });
154
157
  it("should reuse projectId on existing projectid", async () => {
155
- const repo = await mockRepo();
158
+ const ciTestRepo = JSON.parse(readFileSync("./mocks/ci-test-repo-no-shallow.json", { encoding: "utf-8" }));
159
+ const repo = await mockRepo({ fromSnapshot: ciTestRepo });
156
160
  repo.nodeishFs.writeFile("/project.inlang/project_id", "testId");
157
161
  const result = await tryCatch(() => loadProject({
158
162
  projectPath: "/project.inlang",
159
- nodeishFs: repo.nodeishFs,
160
163
  repo,
161
164
  _import,
162
165
  }));
@@ -0,0 +1,16 @@
1
+ import type { Repository } from "@lix-js/client";
2
+ /**
3
+ * Creates a project id if it does not exist yet.
4
+ *
5
+ * - this is a migration to ensure that all projects have a project id
6
+ * - new projects are created with a project id (in the future)
7
+ */
8
+ export declare function maybeCreateFirstProjectId(args: {
9
+ projectPath: string;
10
+ repo?: Repository;
11
+ }): Promise<void>;
12
+ export declare function generateProjectId(args: {
13
+ repo?: Repository;
14
+ projectPath: string;
15
+ }): Promise<string | undefined>;
16
+ //# sourceMappingURL=maybeCreateFirstProjectId.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maybeCreateFirstProjectId.d.ts","sourceRoot":"","sources":["../../src/migrations/maybeCreateFirstProjectId.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAGhD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACrD,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,UAAU,CAAA;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBhB;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,UAAU,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,+BAWvF"}
@@ -0,0 +1,38 @@
1
+ import { hash } from "@lix-js/client";
2
+ /**
3
+ * Creates a project id if it does not exist yet.
4
+ *
5
+ * - this is a migration to ensure that all projects have a project id
6
+ * - new projects are created with a project id (in the future)
7
+ */
8
+ export async function maybeCreateFirstProjectId(args) {
9
+ // the migration assumes a repository
10
+ if (args.repo === undefined) {
11
+ return;
12
+ }
13
+ try {
14
+ await args.repo.nodeishFs.readFile(args.projectPath + "/project_id", {
15
+ encoding: "utf-8",
16
+ });
17
+ }
18
+ catch (error) {
19
+ // @ts-ignore
20
+ if (error.code === "ENOENT" && args.repo) {
21
+ const projectId = await generateProjectId({ repo: args.repo, projectPath: args.projectPath });
22
+ if (projectId) {
23
+ await args.repo.nodeishFs.writeFile(args.projectPath + "/project_id", projectId);
24
+ }
25
+ }
26
+ }
27
+ }
28
+ export async function generateProjectId(args) {
29
+ if (!args.repo || !args.projectPath) {
30
+ return undefined;
31
+ }
32
+ const firstCommitHash = await args.repo.getFirstCommitHash();
33
+ const originHash = await args.repo.getOrigin({ safeHashOnly: true });
34
+ if (firstCommitHash) {
35
+ return hash(`${firstCommitHash + args.projectPath + originHash}`);
36
+ }
37
+ return undefined;
38
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=maybeCreateFirstProjectId.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maybeCreateFirstProjectId.test.d.ts","sourceRoot":"","sources":["../../src/migrations/maybeCreateFirstProjectId.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ import { generateProjectId } from "./maybeCreateFirstProjectId.js";
2
+ import { it, expect } from "vitest";
3
+ import { openRepository } from "@lix-js/client/src/openRepository.ts";
4
+ import { mockRepo, createNodeishMemoryFs } from "@lix-js/client";
5
+ import {} from "@lix-js/fs";
6
+ // eslint-disable-next-line no-restricted-imports -- test
7
+ import { readFileSync } from "node:fs";
8
+ const ciTestRepo = JSON.parse(readFileSync("./mocks/ci-test-repo-no-shallow.json", { encoding: "utf-8" }));
9
+ const repo = await mockRepo({ fromSnapshot: ciTestRepo });
10
+ it("should return if repo is undefined", async () => {
11
+ const projectId = await generateProjectId({ repo: undefined, projectPath: "mocked_project_path" });
12
+ expect(projectId).toBeUndefined();
13
+ });
14
+ it("should generate a project id", async () => {
15
+ const projectId = await generateProjectId({ repo, projectPath: "mocked_project_path" });
16
+ expect(projectId).toBe("959bcf0a30e678c9b90a3c76d1a281d085eab55f289c5439b6b10849baa1920c");
17
+ });
18
+ it("should return undefined if repoMeta contains error", async () => {
19
+ const repoWithError = await openRepository("https://github.com/inlang/no-exist", {
20
+ nodeishFs: createNodeishMemoryFs(),
21
+ });
22
+ const projectId = await generateProjectId({
23
+ repo: repoWithError,
24
+ projectPath: "mocked_project_path",
25
+ });
26
+ expect(projectId).toBeUndefined();
27
+ });
@@ -1,7 +1,7 @@
1
1
  import type { NodeishFilesystem } from "@lix-js/fs";
2
2
  /**
3
3
  * Migrates to the new project directory structure
4
- * https://github.com/inlang/monorepo/issues/1678
4
+ * https://github.com/opral/monorepo/issues/1678
5
5
  */
6
6
  export declare const maybeMigrateToDirectory: (args: {
7
7
  nodeishFs: NodeishFilesystem;
@@ -1,7 +1,7 @@
1
1
  import { tryCatch } from "@inlang/result";
2
2
  /**
3
3
  * Migrates to the new project directory structure
4
- * https://github.com/inlang/monorepo/issues/1678
4
+ * https://github.com/opral/monorepo/issues/1678
5
5
  */
6
6
  export const maybeMigrateToDirectory = async (args) => {
7
7
  // the migration assumes that the projectPath ends with project.inlang
@@ -38,9 +38,9 @@ The \`project.inlang.json\` file is now contained in a project directory e.g. \`
38
38
  ## Why is this happening?
39
39
 
40
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.
41
+ and the following GitHub issue for more information https://github.com/opral/monorepo/issues/1678.
42
42
 
43
- - Monorepo support https://github.com/inlang/monorepo/discussions/258.
43
+ - Monorepo support https://github.com/opral/monorepo/discussions/258.
44
44
  - Required for many other future features like caching, first class offline support, and more.
45
45
  - Stablize the inlang project format.
46
46
  `;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * List of telemetry events for typesafety.
3
+ *
4
+ * - prefix with `SDK` to avoid collisions with other apps
5
+ * - use past tense to indicate that the event is completed
6
+ */
7
+ declare const events: readonly ["SDK loaded project"];
8
+ /**
9
+ * Capture an event.
10
+ *
11
+ * - manually calling the PostHog API because the SDKs were not platform angostic (and generally bloated)
12
+ */
13
+ export declare const capture: (event: (typeof events)[number], args: {
14
+ projectId: string;
15
+ /**
16
+ * Please use snake_case for property names.
17
+ */
18
+ properties: Record<string, any>;
19
+ }) => Promise<void>;
20
+ export {};
21
+ //# sourceMappingURL=capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/telemetry/capture.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,QAAA,MAAM,MAAM,iCAAkC,CAAA;AAE9C;;;;GAIG;AACH,eAAO,MAAM,OAAO,UACZ,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC,QACxB;IACL,SAAS,EAAE,MAAM,CAAA;IACjB;;OAEG;IACH,UAAU,EAAE,OAAO,MAAM,EAAE,GAAG,CAAC,CAAA;CAC/B,kBAyBD,CAAA"}
@@ -0,0 +1,39 @@
1
+ import { ENV_VARIABLES } from "../env-variables/index.js";
2
+ /**
3
+ * List of telemetry events for typesafety.
4
+ *
5
+ * - prefix with `SDK` to avoid collisions with other apps
6
+ * - use past tense to indicate that the event is completed
7
+ */
8
+ const events = ["SDK loaded project"];
9
+ /**
10
+ * Capture an event.
11
+ *
12
+ * - manually calling the PostHog API because the SDKs were not platform angostic (and generally bloated)
13
+ */
14
+ export const capture = async (event, args) => {
15
+ // do not send events if the token is not set
16
+ // (assuming this eases testing)
17
+ if (ENV_VARIABLES.PUBLIC_POSTHOG_TOKEN === undefined) {
18
+ return;
19
+ }
20
+ try {
21
+ await fetch("https://eu.posthog.com/capture/", {
22
+ method: "POST",
23
+ body: JSON.stringify({
24
+ api_key: ENV_VARIABLES.PUBLIC_POSTHOG_TOKEN,
25
+ event,
26
+ // id is "unknown" because no user information is available
27
+ distinct_id: "unknown",
28
+ properties: {
29
+ $groups: { project: args.projectId },
30
+ ...args.properties,
31
+ },
32
+ }),
33
+ });
34
+ }
35
+ catch (e) {
36
+ // TODO implement sentry logging
37
+ // do not console.log and avoid exposing internal errors to the user
38
+ }
39
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inlang/sdk",
3
3
  "type": "module",
4
- "version": "0.22.0",
4
+ "version": "0.23.0",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -26,16 +26,16 @@
26
26
  "solid-js": "1.6.12",
27
27
  "throttle-debounce": "^5.0.0",
28
28
  "@inlang/json-types": "1.1.0",
29
- "@inlang/language-tag": "1.3.0",
30
- "@inlang/message-lint-rule": "1.4.0",
31
- "@inlang/message": "2.0.0",
32
- "@inlang/module": "1.2.1",
33
- "@inlang/plugin": "2.4.1",
34
- "@inlang/project-settings": "2.2.0",
35
- "@inlang/result": "1.1.0",
36
- "@inlang/translatable": "1.2.0",
29
+ "@inlang/language-tag": "1.4.0",
30
+ "@inlang/message": "2.0.1",
31
+ "@inlang/message-lint-rule": "1.4.1",
32
+ "@inlang/module": "1.2.2",
33
+ "@inlang/plugin": "2.4.2",
34
+ "@inlang/project-settings": "2.2.1",
35
+ "@inlang/translatable": "1.2.1",
36
+ "@lix-js/fs": "0.5.0",
37
37
  "@lix-js/client": "0.5.0",
38
- "@lix-js/fs": "0.5.0"
38
+ "@inlang/result": "1.1.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/throttle-debounce": "5.0.0",
@@ -47,8 +47,9 @@
47
47
  "vitest": "^0.33.0"
48
48
  },
49
49
  "scripts": {
50
- "build": "tsc --build",
51
- "dev": "tsc --watch",
50
+ "build": "npm run prepare-env-variables && tsc --build",
51
+ "dev": "npm run prepare-env-variables && tsc --watch",
52
+ "prepare-env-variables": "node ./src/env-variables/createIndexFile.js",
52
53
  "test": "tsc --noEmit && vitest run --passWithNoTests --coverage",
53
54
  "lint": "eslint ./src --fix",
54
55
  "format": "prettier ./src --write",
@@ -28,7 +28,7 @@ export const createNodeishFsWithWatcher = (args: {
28
28
  }
29
29
  } catch (err: any) {
30
30
  if (err.name === "AbortError") return
31
- // https://github.com/inlang/monorepo/issues/1647
31
+ // https://github.com/opral/monorepo/issues/1647
32
32
  // the file does not exist (yet)
33
33
  // this is not testable beacause the fs.watch api differs
34
34
  // from node and lix. lenghty
@@ -0,0 +1 @@
1
+ index.js
@@ -0,0 +1,28 @@
1
+ /* eslint-disable no-restricted-imports */
2
+ /* eslint-disable no-undef */
3
+ /**
4
+ * This script writes public environment variables
5
+ * to an importable env file.
6
+ *
7
+ * - The SDK must bundle this file with the rest of the SDK
8
+ * - This scripts avoids the need for a bundler
9
+ * - Must be ran before building the SDK
10
+ */
11
+
12
+ import fs from "node:fs/promises"
13
+ import url from "node:url"
14
+ import path from "node:path"
15
+
16
+ const dirname = path.dirname(url.fileURLToPath(import.meta.url))
17
+
18
+ await fs.writeFile(
19
+ dirname + "/index.ts",
20
+ `
21
+ export const ENV_VARIABLES = {
22
+ PUBLIC_POSTHOG_TOKEN: "${process.env.PUBLIC_POSTHOG_TOKEN}",
23
+ }
24
+ `
25
+ )
26
+
27
+ // eslint-disable-next-line no-console
28
+ console.log("✅ Created env variable index file.")
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Avoiding TypeScript errors before the `createIndexFile` script
3
+ * is invoked by defining the type ahead of time.
4
+ */
5
+
6
+ /**
7
+ * Env variables that are available at runtime.
8
+ *
9
+ * - assume that each env variable might be undefined (to ease development/contributions)
10
+ */
11
+ export declare const ENV_VARIABLES: Partial<{
12
+ PUBLIC_POSTHOG_TOKEN: string
13
+ }>
@@ -1,7 +1,10 @@
1
- import { assert, describe, it } from "vitest"
1
+ import { assert, describe, it, expect } from "vitest"
2
2
  import { listProjects } from "./listProjects.js"
3
- import { createNodeishMemoryFs } from "@lix-js/fs"
3
+ import { createNodeishMemoryFs, type Snapshot } from "@lix-js/fs"
4
4
  import type { ProjectSettings } from "@inlang/project-settings"
5
+ import { mockRepo } from "@lix-js/client"
6
+ // eslint-disable-next-line no-restricted-imports -- test
7
+ import { readFileSync } from "node:fs"
5
8
 
6
9
  const settings: ProjectSettings = {
7
10
  sourceLanguageTag: "en",
@@ -55,6 +58,22 @@ describe("listProjects", () => {
55
58
  })
56
59
  })
57
60
 
61
+ it("should not crash on broken symlinks as cal.com has", async () => {
62
+ const ciTestRepo: Snapshot = JSON.parse(
63
+ readFileSync("./mocks/ci-test-repo-no-shallow.json", { encoding: "utf-8" })
64
+ )
65
+ const repo = await mockRepo({ fromSnapshot: ciTestRepo })
66
+ repo.checkout({ branch: "test-symlink" })
67
+
68
+ const link = await repo.nodeishFs.readlink("test-symlink-not-existing-target")
69
+
70
+ expect(link).toBe("/test-symlink-not-existing-target//.././no-exist")
71
+
72
+ await listProjects(repo.nodeishFs, "/").then((projects) => {
73
+ assert(projects.length === 1)
74
+ })
75
+ })
76
+
58
77
  it("should also find files inside of a dir that ends with *.inlang", async () => {
59
78
  const fs = createNodeishMemoryFs()
60
79
  await fs.mkdir("/user/dir1/go.inlang", { recursive: true })
@@ -17,14 +17,21 @@ export const listProjects = async (
17
17
  const files = await nodeishFs.readdir(path)
18
18
  for (const file of files) {
19
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)
20
+ try {
21
+ const stats = await nodeishFs.stat(filePath)
22
+ if (stats.isDirectory()) {
23
+ if (file === "node_modules") {
24
+ continue
25
+ }
26
+
27
+ if (file.endsWith(".inlang")) {
28
+ projects.push({ projectPath: filePath })
29
+ } else {
30
+ await searchDir(filePath, depth + 1)
31
+ }
27
32
  }
33
+ } catch {
34
+ continue
28
35
  }
29
36
  }
30
37
  }
@@ -20,6 +20,9 @@ import { createNodeishMemoryFs, normalizePath } from "@lix-js/fs"
20
20
  import { createMessage } from "./test-utilities/createMessage.js"
21
21
  import { tryCatch } from "@inlang/result"
22
22
  import { mockRepo } from "@lix-js/client"
23
+ import { type Snapshot } from "@lix-js/fs"
24
+ // eslint-disable-next-line no-restricted-imports -- test
25
+ import { readFileSync } from "node:fs"
23
26
 
24
27
  // ------------------------------------------------------------------------------------------------
25
28
 
@@ -161,7 +164,10 @@ describe("initialization", () => {
161
164
  })
162
165
 
163
166
  it("should generate projectId on missing projectid", async () => {
164
- const repo = await mockRepo()
167
+ const ciTestRepo: Snapshot = JSON.parse(
168
+ readFileSync("./mocks/ci-test-repo-no-shallow.json", { encoding: "utf-8" })
169
+ )
170
+ const repo = await mockRepo({ fromSnapshot: ciTestRepo })
165
171
 
166
172
  const existing = await repo.nodeishFs
167
173
  .readFile("/project.inlang/project_id", {
@@ -177,7 +183,6 @@ describe("initialization", () => {
177
183
  const result = await tryCatch(() =>
178
184
  loadProject({
179
185
  projectPath: "/project.inlang",
180
- nodeishFs: repo.nodeishFs,
181
186
  repo,
182
187
  _import,
183
188
  })
@@ -191,21 +196,23 @@ describe("initialization", () => {
191
196
  return { error }
192
197
  })
193
198
 
194
- expect(newId).toBe("7cd6c2b7cf12febf99496408917123fdfe158b6bc442914f5fb42aa74346bd50")
199
+ expect(newId).toBe("e8c61726bc2f437ec6a260abb632b3c59195059b60031e648b5afbafa7f3d79a")
195
200
 
196
201
  expect(result.error).toBeUndefined()
197
202
  expect(result.data).toBeDefined()
198
203
  })
199
204
 
200
205
  it("should reuse projectId on existing projectid", async () => {
201
- const repo = await mockRepo()
206
+ const ciTestRepo: Snapshot = JSON.parse(
207
+ readFileSync("./mocks/ci-test-repo-no-shallow.json", { encoding: "utf-8" })
208
+ )
209
+ const repo = await mockRepo({ fromSnapshot: ciTestRepo })
202
210
 
203
211
  repo.nodeishFs.writeFile("/project.inlang/project_id", "testId")
204
212
 
205
213
  const result = await tryCatch(() =>
206
214
  loadProject({
207
215
  projectPath: "/project.inlang",
208
- nodeishFs: repo.nodeishFs,
209
216
  repo,
210
217
  _import,
211
218
  })
@@ -23,12 +23,13 @@ import { ProjectSettings, Message, type NodeishFilesystemSubset } from "./versio
23
23
  import { tryCatch, type Result } from "@inlang/result"
24
24
  import { migrateIfOutdated } from "@inlang/project-settings/migration"
25
25
  import { createNodeishFsWithAbsolutePaths } from "./createNodeishFsWithAbsolutePaths.js"
26
- import { normalizePath, type NodeishFilesystem } from "@lix-js/fs"
26
+ import { normalizePath } 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 { maybeCreateFirstProjectId } from "./migrations/maybeCreateFirstProjectId.js"
30
31
  import type { Repository } from "@lix-js/client"
31
- import { generateProjectId } from "./generateProjectId.js"
32
+ import { capture } from "./telemetry/capture.js"
32
33
 
33
34
  const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
34
35
 
@@ -36,24 +37,49 @@ const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
36
37
  * Creates an inlang instance.
37
38
  *
38
39
  * @param projectPath - Absolute path to the inlang settings file.
39
- * @param nodeishFs - Filesystem that implements the NodeishFilesystemSubset interface.
40
+ * @param @deprecated nodeishFs - Filesystem that implements the NodeishFilesystemSubset interface.
40
41
  * @param _import - Use `_import` to pass a custom import function for testing,
41
42
  * and supporting legacy resolvedModules such as CJS.
42
- * @param _capture - Use `_capture` to capture events for analytics.
43
43
  *
44
44
  */
45
- export const loadProject = async (args: {
45
+ export async function loadProject(args: {
46
46
  projectPath: string
47
- repo?: Repository
48
- nodeishFs: NodeishFilesystem
47
+ nodeishFs: Repository["nodeishFs"]
48
+ /**
49
+ * The app id is used to identify the app that is using the SDK.
50
+ *
51
+ * We use the app id to group events in telemetry to answer questions
52
+ * like "Which apps causes these errors?" or "Which apps are used more than others?".
53
+ *
54
+ * @example
55
+ * appId: "app.inlang.badge"
56
+ */
57
+ appId?: string
49
58
  _import?: ImportFunction
50
- _capture?: (id: string, props: Record<string, unknown>) => void
51
- }): Promise<InlangProject> => {
52
- const projectPath = normalizePath(args.projectPath)
59
+ }): Promise<InlangProject>
53
60
 
54
- // -- migrate if outdated ------------------------------------------------
61
+ /**
62
+ * @param projectPath - Absolute path to the inlang settings file.
63
+ * @param repo - An instance of a lix repo as returned by `openRepository`.
64
+ * @param _import - Use `_import` to pass a custom import function for testing,
65
+ * and supporting legacy resolvedModules such as CJS.
66
+ *
67
+ */
68
+ export async function loadProject(args: {
69
+ projectPath: string
70
+ repo: Repository
71
+ appId?: string
72
+ _import?: ImportFunction
73
+ }): Promise<InlangProject>
55
74
 
56
- await maybeMigrateToDirectory({ nodeishFs: args.nodeishFs, projectPath })
75
+ export async function loadProject(args: {
76
+ projectPath: string
77
+ repo?: Repository
78
+ appId?: string
79
+ _import?: ImportFunction
80
+ nodeishFs?: Repository["nodeishFs"]
81
+ }): Promise<InlangProject> {
82
+ const projectPath = normalizePath(args.projectPath)
57
83
 
58
84
  // -- validation --------------------------------------------------------
59
85
  // the only place where throwing is acceptable because the project
@@ -72,35 +98,37 @@ export const loadProject = async (args: {
72
98
  )
73
99
  }
74
100
 
75
- // -- load project ------------------------------------------------------
76
- let idError: Error | undefined
77
- return await createRoot(async () => {
78
- const [initialized, markInitAsComplete, markInitAsFailed] = createAwaitable()
79
- const nodeishFs = createNodeishFsWithAbsolutePaths({
80
- projectPath,
81
- nodeishFs: args.nodeishFs,
82
- })
101
+ let fs: Repository["nodeishFs"]
102
+ if (args.nodeishFs) {
103
+ // TODO: deprecate
104
+ fs = args.nodeishFs
105
+ } else if (args.repo) {
106
+ fs = args.repo.nodeishFs
107
+ } else {
108
+ throw new LoadProjectInvalidArgument(`Repo missing from arguments.`, { argument: "repo" })
109
+ }
83
110
 
84
- let projectId: string | undefined
111
+ const nodeishFs = createNodeishFsWithAbsolutePaths({
112
+ projectPath,
113
+ nodeishFs: fs,
114
+ })
85
115
 
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
- }
116
+ // -- migratations ------------------------------------------------
117
+
118
+ await maybeMigrateToDirectory({ nodeishFs: fs, projectPath })
119
+ await maybeCreateFirstProjectId({ projectPath, repo: args.repo })
120
+
121
+ // -- load project ------------------------------------------------------
103
122
 
123
+ return await createRoot(async () => {
124
+ // TODO remove tryCatch after https://github.com/opral/monorepo/issues/2013
125
+ // - a repo will always be present
126
+ // - if a repo is present, the project id will always be present
127
+ const { data: projectId } = await tryCatch(() =>
128
+ fs.readFile(args.projectPath + "/project_id", { encoding: "utf-8" })
129
+ )
130
+
131
+ const [initialized, markInitAsComplete, markInitAsFailed] = createAwaitable()
104
132
  // -- settings ------------------------------------------------------------
105
133
 
106
134
  const [settings, _setSettings] = createSignal<ProjectSettings>()
@@ -113,12 +141,7 @@ export const loadProject = async (args: {
113
141
  // }
114
142
 
115
143
  loadSettings({ settingsFilePath: projectPath + "/settings.json", nodeishFs })
116
- .then((settings) => {
117
- setSettings(settings)
118
- // rename settings to get a convenient access to the data in Posthog
119
- const project_settings = settings
120
- args._capture?.("SDK used settings", { project_settings, group: projectId })
121
- })
144
+ .then((settings) => setSettings(settings))
122
145
  .catch((err) => {
123
146
  markInitAsFailed(err)
124
147
  })
@@ -218,7 +241,7 @@ export const loadProject = async (args: {
218
241
  module:
219
242
  resolvedModules()?.meta.find((m) => m.id.includes(rule.id))?.module ??
220
243
  "Unknown module. You stumbled on a bug in inlang's source code. Please open an issue.",
221
- // default to warning, see https://github.com/inlang/monorepo/issues/1254
244
+ // default to warning, see https://github.com/opral/monorepo/issues/1254
222
245
  level: settingsValue["messageLintRuleLevels"]?.[rule.id] ?? "warning",
223
246
  } satisfies InstalledMessageLintRule)
224
247
  ) satisfies Array<InstalledMessageLintRule>
@@ -285,6 +308,29 @@ export const loadProject = async (args: {
285
308
  debouncedSave(messagesQuery.getAll())
286
309
  })
287
310
 
311
+ /**
312
+ * Utility to escape reactive tracking and avoid multiple calls to
313
+ * the capture event.
314
+ *
315
+ * Should be addressed with https://github.com/opral/monorepo/issues/1772
316
+ */
317
+ let projectLoadedCapturedAlready = false
318
+
319
+ if (projectId && projectLoadedCapturedAlready === false) {
320
+ projectLoadedCapturedAlready = true
321
+ // TODO ensure that capture is "awaited" without blocking the the app from starting
322
+ await capture("SDK loaded project", {
323
+ projectId,
324
+ properties: {
325
+ appId: args.appId,
326
+ settings: settings(),
327
+ installedPluginIds: installedPlugins().map((p) => p.id),
328
+ installedMessageLintRuleIds: installedMessageLintRules().map((r) => r.id),
329
+ numberOfMessages: messagesQuery.includedMessageIds().length,
330
+ },
331
+ })
332
+ }
333
+
288
334
  return {
289
335
  installed: {
290
336
  plugins: createSubscribable(() => installedPlugins()),
@@ -292,10 +338,7 @@ export const loadProject = async (args: {
292
338
  },
293
339
  errors: createSubscribable(() => [
294
340
  ...(initializeError ? [initializeError] : []),
295
- ...(idError ? [idError] : []),
296
341
  ...(resolvedModules() ? resolvedModules()!.errors : []),
297
- // have a query error exposed
298
- //...(lintErrors() ?? []),
299
342
  ]),
300
343
  settings: createSubscribable(() => settings() as ProjectSettings),
301
344
  setSettings,
@@ -0,0 +1,34 @@
1
+ import { generateProjectId } from "./maybeCreateFirstProjectId.js"
2
+ import { it, expect } from "vitest"
3
+ import { openRepository } from "@lix-js/client/src/openRepository.ts"
4
+ import { mockRepo, createNodeishMemoryFs } from "@lix-js/client"
5
+ import { type Snapshot } from "@lix-js/fs"
6
+ // eslint-disable-next-line no-restricted-imports -- test
7
+ import { readFileSync } from "node:fs"
8
+
9
+ const ciTestRepo: Snapshot = JSON.parse(
10
+ readFileSync("./mocks/ci-test-repo-no-shallow.json", { encoding: "utf-8" })
11
+ )
12
+ const repo = await mockRepo({ fromSnapshot: ciTestRepo as Snapshot })
13
+
14
+ it("should return if repo is undefined", async () => {
15
+ const projectId = await generateProjectId({ repo: undefined, projectPath: "mocked_project_path" })
16
+ expect(projectId).toBeUndefined()
17
+ })
18
+
19
+ it("should generate a project id", async () => {
20
+ const projectId = await generateProjectId({ repo, projectPath: "mocked_project_path" })
21
+ expect(projectId).toBe("959bcf0a30e678c9b90a3c76d1a281d085eab55f289c5439b6b10849baa1920c")
22
+ })
23
+
24
+ it("should return undefined if repoMeta contains error", async () => {
25
+ const repoWithError = await openRepository("https://github.com/inlang/no-exist", {
26
+ nodeishFs: createNodeishMemoryFs(),
27
+ })
28
+
29
+ const projectId = await generateProjectId({
30
+ repo: repoWithError,
31
+ projectPath: "mocked_project_path",
32
+ })
33
+ expect(projectId).toBeUndefined()
34
+ })
@@ -0,0 +1,44 @@
1
+ import type { Repository } from "@lix-js/client"
2
+ import { hash } from "@lix-js/client"
3
+
4
+ /**
5
+ * Creates a project id if it does not exist yet.
6
+ *
7
+ * - this is a migration to ensure that all projects have a project id
8
+ * - new projects are created with a project id (in the future)
9
+ */
10
+ export async function maybeCreateFirstProjectId(args: {
11
+ projectPath: string
12
+ repo?: Repository
13
+ }): Promise<void> {
14
+ // the migration assumes a repository
15
+ if (args.repo === undefined) {
16
+ return
17
+ }
18
+ try {
19
+ await args.repo.nodeishFs.readFile(args.projectPath + "/project_id", {
20
+ encoding: "utf-8",
21
+ })
22
+ } catch (error) {
23
+ // @ts-ignore
24
+ if (error.code === "ENOENT" && args.repo) {
25
+ const projectId = await generateProjectId({ repo: args.repo, projectPath: args.projectPath })
26
+ if (projectId) {
27
+ await args.repo.nodeishFs.writeFile(args.projectPath + "/project_id", projectId)
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ export async function generateProjectId(args: { repo?: Repository; projectPath: string }) {
34
+ if (!args.repo || !args.projectPath) {
35
+ return undefined
36
+ }
37
+ const firstCommitHash = await args.repo.getFirstCommitHash()
38
+ const originHash = await args.repo.getOrigin({ safeHashOnly: true })
39
+
40
+ if (firstCommitHash) {
41
+ return hash(`${firstCommitHash + args.projectPath + originHash}`)
42
+ }
43
+ return undefined
44
+ }
@@ -3,7 +3,7 @@ import type { NodeishFilesystem } from "@lix-js/fs"
3
3
 
4
4
  /**
5
5
  * Migrates to the new project directory structure
6
- * https://github.com/inlang/monorepo/issues/1678
6
+ * https://github.com/opral/monorepo/issues/1678
7
7
  */
8
8
  export const maybeMigrateToDirectory = async (args: {
9
9
  nodeishFs: NodeishFilesystem
@@ -51,9 +51,9 @@ The \`project.inlang.json\` file is now contained in a project directory e.g. \`
51
51
  ## Why is this happening?
52
52
 
53
53
  See this RFC https://docs.google.com/document/d/1OYyA1wYfQRbIJOIBDliYoWjiUlkFBNxH_U2R4WpVRZ4/edit#heading=h.pecv6xb7ial6
54
- and the following GitHub issue for more information https://github.com/inlang/monorepo/issues/1678.
54
+ and the following GitHub issue for more information https://github.com/opral/monorepo/issues/1678.
55
55
 
56
- - Monorepo support https://github.com/inlang/monorepo/discussions/258.
56
+ - Monorepo support https://github.com/opral/monorepo/discussions/258.
57
57
  - Required for many other future features like caching, first class offline support, and more.
58
58
  - Stablize the inlang project format.
59
59
  `
@@ -0,0 +1,49 @@
1
+ import { ENV_VARIABLES } from "../env-variables/index.js"
2
+
3
+ /**
4
+ * List of telemetry events for typesafety.
5
+ *
6
+ * - prefix with `SDK` to avoid collisions with other apps
7
+ * - use past tense to indicate that the event is completed
8
+ */
9
+ const events = ["SDK loaded project"] as const
10
+
11
+ /**
12
+ * Capture an event.
13
+ *
14
+ * - manually calling the PostHog API because the SDKs were not platform angostic (and generally bloated)
15
+ */
16
+ export const capture = async (
17
+ event: (typeof events)[number],
18
+ args: {
19
+ projectId: string
20
+ /**
21
+ * Please use snake_case for property names.
22
+ */
23
+ properties: Record<string, any>
24
+ }
25
+ ) => {
26
+ // do not send events if the token is not set
27
+ // (assuming this eases testing)
28
+ if (ENV_VARIABLES.PUBLIC_POSTHOG_TOKEN === undefined) {
29
+ return
30
+ }
31
+ try {
32
+ await fetch("https://eu.posthog.com/capture/", {
33
+ method: "POST",
34
+ body: JSON.stringify({
35
+ api_key: ENV_VARIABLES.PUBLIC_POSTHOG_TOKEN,
36
+ event,
37
+ // id is "unknown" because no user information is available
38
+ distinct_id: "unknown",
39
+ properties: {
40
+ $groups: { project: args.projectId },
41
+ ...args.properties,
42
+ },
43
+ }),
44
+ })
45
+ } catch (e) {
46
+ // TODO implement sentry logging
47
+ // do not console.log and avoid exposing internal errors to the user
48
+ }
49
+ }
@@ -1,3 +0,0 @@
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
@@ -1 +0,0 @@
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"}
@@ -1,11 +0,0 @@
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
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=generateProjectId.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"generateProjectId.test.d.ts","sourceRoot":"","sources":["../src/generateProjectId.test.ts"],"names":[],"mappings":""}
@@ -1,18 +0,0 @@
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
- });
@@ -1,22 +0,0 @@
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
- })
@@ -1,14 +0,0 @@
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
- }