@inlang/sdk 0.22.0 → 0.24.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 (46) hide show
  1. package/README.md +2 -2
  2. package/dist/adapter/solidAdapter.test.js +13 -6
  3. package/dist/createNodeishFsWithWatcher.js +1 -1
  4. package/dist/env-variables/index.d.ts +4 -0
  5. package/dist/env-variables/index.d.ts.map +1 -0
  6. package/dist/env-variables/index.js +3 -0
  7. package/dist/listProjects.d.ts.map +1 -1
  8. package/dist/listProjects.js +15 -9
  9. package/dist/listProjects.test.js +14 -1
  10. package/dist/loadProject.d.ts +28 -9
  11. package/dist/loadProject.d.ts.map +1 -1
  12. package/dist/loadProject.js +49 -50
  13. package/dist/loadProject.test.js +70 -33
  14. package/dist/migrations/maybeCreateFirstProjectId.d.ts +16 -0
  15. package/dist/migrations/maybeCreateFirstProjectId.d.ts.map +1 -0
  16. package/dist/migrations/maybeCreateFirstProjectId.js +37 -0
  17. package/dist/migrations/maybeCreateFirstProjectId.test.d.ts +2 -0
  18. package/dist/migrations/maybeCreateFirstProjectId.test.d.ts.map +1 -0
  19. package/dist/migrations/maybeCreateFirstProjectId.test.js +27 -0
  20. package/dist/migrations/migrateToDirectory.d.ts +1 -1
  21. package/dist/migrations/migrateToDirectory.js +3 -3
  22. package/dist/telemetry/capture.d.ts +21 -0
  23. package/dist/telemetry/capture.d.ts.map +1 -0
  24. package/dist/telemetry/capture.js +39 -0
  25. package/package.json +13 -12
  26. package/src/adapter/solidAdapter.test.ts +13 -6
  27. package/src/createNodeishFsWithWatcher.ts +1 -1
  28. package/src/env-variables/.prettierignore +1 -0
  29. package/src/env-variables/createIndexFile.js +28 -0
  30. package/src/env-variables/index.d.ts +13 -0
  31. package/src/listProjects.test.ts +21 -2
  32. package/src/listProjects.ts +14 -7
  33. package/src/loadProject.test.ts +73 -33
  34. package/src/loadProject.ts +91 -48
  35. package/src/migrations/maybeCreateFirstProjectId.test.ts +34 -0
  36. package/src/migrations/maybeCreateFirstProjectId.ts +43 -0
  37. package/src/migrations/migrateToDirectory.ts +3 -3
  38. package/src/telemetry/capture.ts +49 -0
  39. package/dist/generateProjectId.d.ts +0 -3
  40. package/dist/generateProjectId.d.ts.map +0 -1
  41. package/dist/generateProjectId.js +0 -11
  42. package/dist/generateProjectId.test.d.ts +0 -2
  43. package/dist/generateProjectId.test.d.ts.map +0 -1
  44. package/dist/generateProjectId.test.js +0 -18
  45. package/src/generateProjectId.test.ts +0 -22
  46. package/src/generateProjectId.ts +0 -14
@@ -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("432d7ef29c510e99d95e2d14ef57a0797a1603859b5a851b7dff7e77161b8c08")
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,43 @@
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
+
39
+ if (firstCommitHash) {
40
+ return hash(`${firstCommitHash + args.projectPath}`)
41
+ }
42
+ return undefined
43
+ }
@@ -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
- }