@inlang/sdk 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter/solidAdapter.test.js +16 -15
  3. package/dist/createMessageLintReportsQuery.d.ts +2 -3
  4. package/dist/createMessageLintReportsQuery.d.ts.map +1 -1
  5. package/dist/createMessageLintReportsQuery.js +42 -21
  6. package/dist/createNodeishFsWithAbsolutePaths.d.ts +2 -2
  7. package/dist/createNodeishFsWithAbsolutePaths.d.ts.map +1 -1
  8. package/dist/createNodeishFsWithAbsolutePaths.js +4 -4
  9. package/dist/createNodeishFsWithAbsolutePaths.test.js +4 -4
  10. package/dist/createNodeishFsWithWatcher.d.ts +1 -1
  11. package/dist/createNodeishFsWithWatcher.d.ts.map +1 -1
  12. package/dist/createNodeishFsWithWatcher.js +6 -4
  13. package/dist/generateProjectId.d.ts +3 -0
  14. package/dist/generateProjectId.d.ts.map +1 -0
  15. package/dist/generateProjectId.js +11 -0
  16. package/dist/generateProjectId.test.d.ts +2 -0
  17. package/dist/generateProjectId.test.d.ts.map +1 -0
  18. package/dist/generateProjectId.test.js +18 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -0
  22. package/dist/isAbsolutePath.test.js +1 -1
  23. package/dist/listProjects.d.ts +5 -0
  24. package/dist/listProjects.d.ts.map +1 -0
  25. package/dist/listProjects.js +31 -0
  26. package/dist/listProjects.test.d.ts +2 -0
  27. package/dist/listProjects.test.d.ts.map +1 -0
  28. package/dist/listProjects.test.js +56 -0
  29. package/dist/loadProject.d.ts +6 -4
  30. package/dist/loadProject.d.ts.map +1 -1
  31. package/dist/loadProject.js +58 -22
  32. package/dist/loadProject.test.js +157 -75
  33. package/dist/migrations/migrateToDirectory.d.ts +10 -0
  34. package/dist/migrations/migrateToDirectory.d.ts.map +1 -0
  35. package/dist/migrations/migrateToDirectory.js +46 -0
  36. package/dist/migrations/migrateToDirectory.test.d.ts +2 -0
  37. package/dist/migrations/migrateToDirectory.test.d.ts.map +1 -0
  38. package/dist/migrations/migrateToDirectory.test.js +48 -0
  39. package/dist/resolve-modules/validateModuleSettings.test.js +1 -1
  40. package/package.json +57 -56
  41. package/src/adapter/solidAdapter.test.ts +16 -15
  42. package/src/createMessageLintReportsQuery.ts +57 -28
  43. package/src/createNodeishFsWithAbsolutePaths.test.ts +4 -4
  44. package/src/createNodeishFsWithAbsolutePaths.ts +5 -5
  45. package/src/createNodeishFsWithWatcher.ts +6 -4
  46. package/src/generateProjectId.test.ts +22 -0
  47. package/src/generateProjectId.ts +14 -0
  48. package/src/index.ts +1 -0
  49. package/src/isAbsolutePath.test.ts +1 -1
  50. package/src/listProjects.test.ts +69 -0
  51. package/src/listProjects.ts +39 -0
  52. package/src/loadProject.test.ts +182 -76
  53. package/src/loadProject.ts +77 -28
  54. package/src/migrations/migrateToDirectory.test.ts +54 -0
  55. package/src/migrations/migrateToDirectory.ts +59 -0
  56. package/src/resolve-modules/validateModuleSettings.test.ts +1 -1
@@ -23,16 +23,19 @@ 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 } from "@lix-js/fs"
26
+ import { normalizePath, type NodeishFilesystem } from "@lix-js/fs"
27
27
  import { isAbsolutePath } from "./isAbsolutePath.js"
28
28
  import { createNodeishFsWithWatcher } from "./createNodeishFsWithWatcher.js"
29
+ import { maybeMigrateToDirectory } from "./migrations/migrateToDirectory.js"
30
+ import type { Repository } from "@lix-js/client"
31
+ import { generateProjectId } from "./generateProjectId.js"
29
32
 
30
33
  const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
31
34
 
32
35
  /**
33
36
  * Creates an inlang instance.
34
37
  *
35
- * @param settingsFilePath - Absolute path to the inlang settings file.
38
+ * @param projectPath - Absolute path to the inlang settings file.
36
39
  * @param nodeishFs - Filesystem that implements the NodeishFilesystemSubset interface.
37
40
  * @param _import - Use `_import` to pass a custom import function for testing,
38
41
  * and supporting legacy resolvedModules such as CJS.
@@ -40,42 +43,81 @@ const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
40
43
  *
41
44
  */
42
45
  export const loadProject = async (args: {
43
- settingsFilePath: string
44
- nodeishFs: NodeishFilesystemSubset
46
+ projectPath: string
47
+ repo?: Repository
48
+ nodeishFs: NodeishFilesystem
45
49
  _import?: ImportFunction
46
50
  _capture?: (id: string, props: Record<string, unknown>) => void
47
51
  }): Promise<InlangProject> => {
52
+ const projectPath = normalizePath(args.projectPath)
53
+
54
+ // -- migrate if outdated ------------------------------------------------
55
+
56
+ await maybeMigrateToDirectory({ nodeishFs: args.nodeishFs, projectPath })
57
+
48
58
  // -- validation --------------------------------------------------------
49
- //! the only place where throwing is acceptable because the project
50
- //! won't even be loaded. do not throw anywhere else. otherwise, apps
51
- //! can't handle errors gracefully.
52
- if (!isAbsolutePath(args.settingsFilePath)) {
59
+ // the only place where throwing is acceptable because the project
60
+ // won't even be loaded. do not throw anywhere else. otherwise, apps
61
+ // can't handle errors gracefully.
62
+
63
+ if (!isAbsolutePath(args.projectPath)) {
64
+ throw new LoadProjectInvalidArgument(
65
+ `Expected an absolute path but received "${args.projectPath}".`,
66
+ { argument: "projectPath" }
67
+ )
68
+ } else if (/[^\\/]+\.inlang$/.test(projectPath) === false) {
53
69
  throw new LoadProjectInvalidArgument(
54
- `Expected an absolute path but received "${args.settingsFilePath}".`,
55
- { argument: "settingsFilePath" }
70
+ `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`,
71
+ { argument: "projectPath" }
56
72
  )
57
73
  }
58
74
 
59
- const settingsFilePath = normalizePath(args.settingsFilePath)
60
-
61
75
  // -- load project ------------------------------------------------------
76
+ let idError: Error | undefined
62
77
  return await createRoot(async () => {
63
78
  const [initialized, markInitAsComplete, markInitAsFailed] = createAwaitable()
64
79
  const nodeishFs = createNodeishFsWithAbsolutePaths({
65
- settingsFilePath,
80
+ projectPath,
66
81
  nodeishFs: args.nodeishFs,
67
82
  })
68
83
 
84
+ let projectId: string | undefined
85
+
86
+ try {
87
+ projectId = await nodeishFs.readFile(projectPath + "/project_id", {
88
+ encoding: "utf-8",
89
+ })
90
+ } catch (error) {
91
+ // @ts-ignore
92
+ if (error.code === "ENOENT") {
93
+ if (args.repo) {
94
+ projectId = await generateProjectId(args.repo, projectPath)
95
+ if (projectId) {
96
+ await nodeishFs.writeFile(projectPath + "/project_id", projectId)
97
+ }
98
+ }
99
+ } else {
100
+ idError = error as Error
101
+ }
102
+ }
103
+
69
104
  // -- settings ------------------------------------------------------------
70
105
 
71
106
  const [settings, _setSettings] = createSignal<ProjectSettings>()
72
107
  createEffect(() => {
73
- loadSettings({ settingsFilePath, nodeishFs })
108
+ // TODO:
109
+ // if (projectId) {
110
+ // telemetryBrowser.group("project", projectId, {
111
+ // name: projectId,
112
+ // })
113
+ // }
114
+
115
+ loadSettings({ settingsFilePath: projectPath + "/settings.json", nodeishFs })
74
116
  .then((settings) => {
75
117
  setSettings(settings)
76
118
  // rename settings to get a convenient access to the data in Posthog
77
119
  const project_settings = settings
78
- args._capture?.("SDK used settings", { project_settings })
120
+ args._capture?.("SDK used settings", { project_settings, group: projectId })
79
121
  })
80
122
  .catch((err) => {
81
123
  markInitAsFailed(err)
@@ -84,7 +126,7 @@ export const loadProject = async (args: {
84
126
  // TODO: create FS watcher and update settings on change
85
127
 
86
128
  const writeSettingsToDisk = skipFirst((settings: ProjectSettings) =>
87
- _writeSettingsToDisk({ nodeishFs, settings })
129
+ _writeSettingsToDisk({ nodeishFs, settings, projectPath })
88
130
  )
89
131
 
90
132
  const setSettings = (settings: ProjectSettings): Result<void, ProjectSettingsInvalidError> => {
@@ -126,12 +168,11 @@ export const loadProject = async (args: {
126
168
  let settingsValue: ProjectSettings
127
169
  createEffect(() => (settingsValue = settings()!)) // workaround to not run effects twice (e.g. settings change + modules change) (I'm sure there exists a solid way of doing this, but I haven't found it yet)
128
170
 
171
+ // please don't use this as source of truth, use the query instead
172
+ // needed for granular linting
129
173
  const [messages, setMessages] = createSignal<Message[]>()
130
174
 
131
175
  createEffect(() => {
132
- const conf = settings()
133
- if (!conf) return
134
-
135
176
  const _resolvedModules = resolvedModules()
136
177
  if (!_resolvedModules) return
137
178
 
@@ -199,12 +240,16 @@ export const loadProject = async (args: {
199
240
 
200
241
  const initializeError: Error | undefined = await initialized.catch((error) => error)
201
242
 
243
+ const abortController = new AbortController()
244
+ const hasWatcher = nodeishFs.watch("/", { signal: abortController.signal }) !== undefined
245
+
202
246
  const messagesQuery = createMessagesQuery(() => messages() || [])
203
247
  const lintReportsQuery = createMessageLintReportsQuery(
204
- messages,
248
+ messagesQuery,
205
249
  settings as () => ProjectSettings,
206
250
  installedMessageLintRules,
207
- resolvedModules
251
+ resolvedModules,
252
+ hasWatcher
208
253
  )
209
254
 
210
255
  const debouncedSave = skipFirst(
@@ -223,12 +268,14 @@ export const loadProject = async (args: {
223
268
  cause: err,
224
269
  })
225
270
  }
226
- // if (
227
- // newMessages.length !== 0 &&
228
- // JSON.stringify(newMessages) !== JSON.stringify(messages())
229
- // ) {
230
- // setMessages(newMessages)
231
- // }
271
+ const abortController = new AbortController()
272
+ if (
273
+ newMessages.length !== 0 &&
274
+ JSON.stringify(newMessages) !== JSON.stringify(messages()) &&
275
+ nodeishFs.watch("/", { signal: abortController.signal }) !== undefined
276
+ ) {
277
+ setMessages(newMessages)
278
+ }
232
279
  },
233
280
  { atBegin: false }
234
281
  )
@@ -245,6 +292,7 @@ export const loadProject = async (args: {
245
292
  },
246
293
  errors: createSubscribable(() => [
247
294
  ...(initializeError ? [initializeError] : []),
295
+ ...(idError ? [idError] : []),
248
296
  ...(resolvedModules() ? resolvedModules()!.errors : []),
249
297
  // have a query error exposed
250
298
  //...(lintErrors() ?? []),
@@ -320,6 +368,7 @@ const parseSettings = (settings: unknown) => {
320
368
  }
321
369
 
322
370
  const _writeSettingsToDisk = async (args: {
371
+ projectPath: string
323
372
  nodeishFs: NodeishFilesystemSubset
324
373
  settings: ProjectSettings
325
374
  }) => {
@@ -332,7 +381,7 @@ const _writeSettingsToDisk = async (args: {
332
381
  }
333
382
 
334
383
  const { error: writeSettingsError } = await tryCatch(async () =>
335
- args.nodeishFs.writeFile("./project.inlang.json", serializedSettings)
384
+ args.nodeishFs.writeFile(args.projectPath + "/settings.json", serializedSettings)
336
385
  )
337
386
 
338
387
  if (writeSettingsError) {
@@ -0,0 +1,54 @@
1
+ import { test, expect, vi } from "vitest"
2
+ import { maybeMigrateToDirectory } from "./migrateToDirectory.js"
3
+ import { createNodeishMemoryFs } from "@lix-js/fs"
4
+ import type { ProjectSettings } from "@inlang/project-settings"
5
+
6
+ test("it should return if the settings file has an error (let loadProject handle it)", async () => {
7
+ const projectPath = "./project.inlang"
8
+ const mockFs = {
9
+ stat: vi.fn(() => {}),
10
+ readFile: vi.fn(() => {
11
+ throw Error()
12
+ }),
13
+ }
14
+ await maybeMigrateToDirectory({ nodeishFs: mockFs as any, projectPath })
15
+ // something goes wrong in readFile
16
+ expect(mockFs.readFile).toHaveBeenCalled()
17
+ })
18
+
19
+ test("it should create the project directory if it does not exist and a project settings file exists", async () => {
20
+ const projectPath = "./project.inlang"
21
+ const mockFs = {
22
+ readFile: vi.fn(
23
+ () => `{
24
+ "sourceLanguageTag": "en",
25
+ "languageTags": ["en", "de"],
26
+ "modules": []
27
+ }`
28
+ ),
29
+ stat: vi.fn(() => {
30
+ throw Error()
31
+ }),
32
+ mkdir: vi.fn(),
33
+ writeFile: vi.fn(),
34
+ }
35
+ await maybeMigrateToDirectory({ nodeishFs: mockFs as any, projectPath })
36
+ expect(mockFs.mkdir).toHaveBeenCalled()
37
+ expect(mockFs.writeFile).toHaveBeenCalled()
38
+ })
39
+
40
+ test("it should write the settings file to the new path", async () => {
41
+ const fs = createNodeishMemoryFs()
42
+ const mockSettings: ProjectSettings = {
43
+ sourceLanguageTag: "en",
44
+ languageTags: ["en", "de"],
45
+ modules: [],
46
+ }
47
+ await fs.writeFile("./project.inlang.json", JSON.stringify(mockSettings))
48
+ await maybeMigrateToDirectory({ nodeishFs: fs, projectPath: "./project.inlang" })
49
+ const migratedSettingsFile = await fs.readFile("./project.inlang/settings.json", {
50
+ encoding: "utf-8",
51
+ })
52
+ expect(migratedSettingsFile).toEqual(JSON.stringify(mockSettings))
53
+ expect(await fs.stat("./project.inlang.README.md")).toBeDefined()
54
+ })
@@ -0,0 +1,59 @@
1
+ import { tryCatch } from "@inlang/result"
2
+ import type { NodeishFilesystem } from "@lix-js/fs"
3
+
4
+ /**
5
+ * Migrates to the new project directory structure
6
+ * https://github.com/inlang/monorepo/issues/1678
7
+ */
8
+ export const maybeMigrateToDirectory = async (args: {
9
+ nodeishFs: NodeishFilesystem
10
+ projectPath: string
11
+ }) => {
12
+ // the migration assumes that the projectPath ends with project.inlang
13
+ if (args.projectPath.endsWith("project.inlang") === false) {
14
+ return
15
+ }
16
+
17
+ // we assume that stat will throw when the directory does not exist
18
+ const projectDirectory = await tryCatch(() => args.nodeishFs.stat(args.projectPath))
19
+
20
+ // the migration has already been conducted.
21
+ if (projectDirectory.data) {
22
+ return
23
+ }
24
+
25
+ const settingsFile = await tryCatch(() =>
26
+ args.nodeishFs.readFile(args.projectPath + ".json", { encoding: "utf-8" })
27
+ )
28
+
29
+ // the settings file does not exist or something else is wrong, let loadProject handle it
30
+ if (settingsFile.error) {
31
+ return
32
+ }
33
+
34
+ await args.nodeishFs.mkdir(args.projectPath)
35
+ await args.nodeishFs.writeFile(`${args.projectPath}/settings.json`, settingsFile.data)
36
+ await args.nodeishFs.writeFile(args.projectPath + ".README.md", readme)
37
+ }
38
+
39
+ const readme = `
40
+ # DELETE THE \`project.inlang.json\` FILE
41
+
42
+ The \`project.inlang.json\` file is now contained in a project directory e.g. \`project.inlang/settings.json\`.
43
+
44
+
45
+ ## What you need to do
46
+
47
+ 1. Update the inlang CLI (if you use it) to use the new path \`project.inlang\` instead of \`project.inlang.json\`.
48
+ 2. Delete the \`project.inlang.json\` file.
49
+
50
+
51
+ ## Why is this happening?
52
+
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.
55
+
56
+ - Monorepo support https://github.com/inlang/monorepo/discussions/258.
57
+ - Required for many other future features like caching, first class offline support, and more.
58
+ - Stablize the inlang project format.
59
+ `
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
2
  import { expect, test } from "vitest"
3
- import { Plugin, MessageLintRule } from "@inlang/sdk"
3
+ import { Plugin, MessageLintRule } from "../index.js"
4
4
  // import { createNodeishMemoryFs } from "@lix-js/fs"
5
5
  import { Type } from "@sinclair/typebox"
6
6
  import { validatedModuleSettings } from "./validatedModuleSettings.js"