@inlang/sdk 0.17.0 → 0.19.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/adapter/solidAdapter.test.js +16 -15
  2. package/dist/createNodeishFsWithAbsolutePaths.d.ts +2 -2
  3. package/dist/createNodeishFsWithAbsolutePaths.d.ts.map +1 -1
  4. package/dist/createNodeishFsWithAbsolutePaths.js +5 -4
  5. package/dist/createNodeishFsWithAbsolutePaths.test.js +5 -4
  6. package/dist/createNodeishFsWithWatcher.d.ts +12 -0
  7. package/dist/createNodeishFsWithWatcher.d.ts.map +1 -0
  8. package/dist/createNodeishFsWithWatcher.js +52 -0
  9. package/dist/createNodeishFsWithWatcher.test.d.ts +2 -0
  10. package/dist/createNodeishFsWithWatcher.test.d.ts.map +1 -0
  11. package/dist/createNodeishFsWithWatcher.test.js +32 -0
  12. package/dist/isAbsolutePath.test.js +1 -1
  13. package/dist/loadProject.d.ts +4 -4
  14. package/dist/loadProject.d.ts.map +1 -1
  15. package/dist/loadProject.js +45 -24
  16. package/dist/loadProject.test.js +175 -74
  17. package/dist/migrations/migrateToDirectory.d.ts +10 -0
  18. package/dist/migrations/migrateToDirectory.d.ts.map +1 -0
  19. package/dist/migrations/migrateToDirectory.js +46 -0
  20. package/dist/migrations/migrateToDirectory.test.d.ts +2 -0
  21. package/dist/migrations/migrateToDirectory.test.d.ts.map +1 -0
  22. package/dist/migrations/migrateToDirectory.test.js +48 -0
  23. package/dist/resolve-modules/plugins/resolvePlugins.js +1 -1
  24. package/dist/resolve-modules/plugins/resolvePlugins.test.js +2 -1
  25. package/dist/resolve-modules/plugins/types.d.ts +2 -1
  26. package/dist/resolve-modules/plugins/types.d.ts.map +1 -1
  27. package/dist/resolve-modules/validateModuleSettings.test.js +1 -1
  28. package/package.json +56 -56
  29. package/src/adapter/solidAdapter.test.ts +16 -15
  30. package/src/createNodeishFsWithAbsolutePaths.test.ts +5 -4
  31. package/src/createNodeishFsWithAbsolutePaths.ts +9 -5
  32. package/src/createNodeishFsWithWatcher.test.ts +40 -0
  33. package/src/createNodeishFsWithWatcher.ts +58 -0
  34. package/src/isAbsolutePath.test.ts +1 -1
  35. package/src/loadProject.test.ts +206 -75
  36. package/src/loadProject.ts +61 -31
  37. package/src/migrations/migrateToDirectory.test.ts +54 -0
  38. package/src/migrations/migrateToDirectory.ts +59 -0
  39. package/src/resolve-modules/plugins/resolvePlugins.test.ts +2 -1
  40. package/src/resolve-modules/plugins/resolvePlugins.ts +1 -1
  41. package/src/resolve-modules/plugins/types.ts +5 -2
  42. package/src/resolve-modules/validateModuleSettings.test.ts +1 -1
@@ -23,15 +23,17 @@ 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
+ import { createNodeishFsWithWatcher } from "./createNodeishFsWithWatcher.js"
29
+ import { maybeMigrateToDirectory } from "./migrations/migrateToDirectory.js"
28
30
 
29
31
  const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
30
32
 
31
33
  /**
32
34
  * Creates an inlang instance.
33
35
  *
34
- * @param settingsFilePath - Absolute path to the inlang settings file.
36
+ * @param projectPath - Absolute path to the inlang settings file.
35
37
  * @param nodeishFs - Filesystem that implements the NodeishFilesystemSubset interface.
36
38
  * @param _import - Use `_import` to pass a custom import function for testing,
37
39
  * and supporting legacy resolvedModules such as CJS.
@@ -39,29 +41,39 @@ const settingsCompiler = TypeCompiler.Compile(ProjectSettings)
39
41
  *
40
42
  */
41
43
  export const loadProject = async (args: {
42
- settingsFilePath: string
43
- nodeishFs: NodeishFilesystemSubset
44
+ projectPath: string
45
+ nodeishFs: NodeishFilesystem
44
46
  _import?: ImportFunction
45
47
  _capture?: (id: string, props: Record<string, unknown>) => void
46
48
  }): Promise<InlangProject> => {
49
+ const projectPath = normalizePath(args.projectPath)
50
+
51
+ // -- migrate if outdated ------------------------------------------------
52
+
53
+ await maybeMigrateToDirectory({ nodeishFs: args.nodeishFs, projectPath })
54
+
47
55
  // -- validation --------------------------------------------------------
48
- //! the only place where throwing is acceptable because the project
49
- //! won't even be loaded. do not throw anywhere else. otherwise, apps
50
- //! can't handle errors gracefully.
51
- if (!isAbsolutePath(args.settingsFilePath)) {
56
+ // the only place where throwing is acceptable because the project
57
+ // won't even be loaded. do not throw anywhere else. otherwise, apps
58
+ // can't handle errors gracefully.
59
+
60
+ if (!isAbsolutePath(args.projectPath)) {
61
+ throw new LoadProjectInvalidArgument(
62
+ `Expected an absolute path but received "${args.projectPath}".`,
63
+ { argument: "projectPath" }
64
+ )
65
+ } else if (/[^\\/]+\.inlang$/.test(projectPath) === false) {
52
66
  throw new LoadProjectInvalidArgument(
53
- `Expected an absolute path but received "${args.settingsFilePath}".`,
54
- { argument: "settingsFilePath" }
67
+ `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`,
68
+ { argument: "projectPath" }
55
69
  )
56
70
  }
57
71
 
58
- const settingsFilePath = normalizePath(args.settingsFilePath)
59
-
60
72
  // -- load project ------------------------------------------------------
61
73
  return await createRoot(async () => {
62
74
  const [initialized, markInitAsComplete, markInitAsFailed] = createAwaitable()
63
75
  const nodeishFs = createNodeishFsWithAbsolutePaths({
64
- settingsFilePath,
76
+ projectPath,
65
77
  nodeishFs: args.nodeishFs,
66
78
  })
67
79
 
@@ -69,7 +81,7 @@ export const loadProject = async (args: {
69
81
 
70
82
  const [settings, _setSettings] = createSignal<ProjectSettings>()
71
83
  createEffect(() => {
72
- loadSettings({ settingsFilePath, nodeishFs })
84
+ loadSettings({ settingsFilePath: projectPath + "/settings.json", nodeishFs })
73
85
  .then((settings) => {
74
86
  setSettings(settings)
75
87
  // rename settings to get a convenient access to the data in Posthog
@@ -83,7 +95,7 @@ export const loadProject = async (args: {
83
95
  // TODO: create FS watcher and update settings on change
84
96
 
85
97
  const writeSettingsToDisk = skipFirst((settings: ProjectSettings) =>
86
- _writeSettingsToDisk({ nodeishFs, settings })
98
+ _writeSettingsToDisk({ nodeishFs, settings, projectPath })
87
99
  )
88
100
 
89
101
  const setSettings = (settings: ProjectSettings): Result<void, ProjectSettingsInvalidError> => {
@@ -126,6 +138,7 @@ export const loadProject = async (args: {
126
138
  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)
127
139
 
128
140
  const [messages, setMessages] = createSignal<Message[]>()
141
+
129
142
  createEffect(() => {
130
143
  const conf = settings()
131
144
  if (!conf) return
@@ -138,16 +151,28 @@ export const loadProject = async (args: {
138
151
  return
139
152
  }
140
153
 
141
- makeTrulyAsync(
142
- _resolvedModules.resolvedPluginApi.loadMessages({
143
- settings: settingsValue,
144
- })
145
- )
146
- .then((messages) => {
147
- setMessages(messages)
148
- markInitAsComplete()
149
- })
150
- .catch((err) => markInitAsFailed(new PluginLoadMessagesError({ cause: err })))
154
+ const loadAndSetMessages = async (fs: NodeishFilesystemSubset) => {
155
+ makeTrulyAsync(
156
+ _resolvedModules.resolvedPluginApi.loadMessages({
157
+ settings: settingsValue,
158
+ nodeishFs: fs,
159
+ })
160
+ )
161
+ .then((messages) => {
162
+ setMessages(messages)
163
+ markInitAsComplete()
164
+ })
165
+ .catch((err) => markInitAsFailed(new PluginLoadMessagesError({ cause: err })))
166
+ }
167
+
168
+ const fsWithWatcher = createNodeishFsWithWatcher({
169
+ nodeishFs: nodeishFs,
170
+ updateMessages: () => {
171
+ loadAndSetMessages(nodeishFs)
172
+ },
173
+ })
174
+
175
+ loadAndSetMessages(fsWithWatcher)
151
176
  })
152
177
 
153
178
  // -- installed items ----------------------------------------------------
@@ -198,18 +223,22 @@ export const loadProject = async (args: {
198
223
  500,
199
224
  async (newMessages) => {
200
225
  try {
201
- await resolvedModules()?.resolvedPluginApi.saveMessages({
202
- settings: settingsValue,
203
- messages: newMessages,
204
- })
226
+ if (JSON.stringify(newMessages) !== JSON.stringify(messages())) {
227
+ await resolvedModules()?.resolvedPluginApi.saveMessages({
228
+ settings: settingsValue,
229
+ messages: newMessages,
230
+ })
231
+ }
205
232
  } catch (err) {
206
233
  throw new PluginSaveMessagesError({
207
234
  cause: err,
208
235
  })
209
236
  }
237
+ const abortController = new AbortController()
210
238
  if (
211
239
  newMessages.length !== 0 &&
212
- JSON.stringify(newMessages) !== JSON.stringify(messages())
240
+ JSON.stringify(newMessages) !== JSON.stringify(messages()) &&
241
+ nodeishFs.watch("/", { signal: abortController.signal }) === undefined
213
242
  ) {
214
243
  setMessages(newMessages)
215
244
  }
@@ -304,6 +333,7 @@ const parseSettings = (settings: unknown) => {
304
333
  }
305
334
 
306
335
  const _writeSettingsToDisk = async (args: {
336
+ projectPath: string
307
337
  nodeishFs: NodeishFilesystemSubset
308
338
  settings: ProjectSettings
309
339
  }) => {
@@ -316,7 +346,7 @@ const _writeSettingsToDisk = async (args: {
316
346
  }
317
347
 
318
348
  const { error: writeSettingsError } = await tryCatch(async () =>
319
- args.nodeishFs.writeFile("./project.inlang.json", serializedSettings)
349
+ args.nodeishFs.writeFile(args.projectPath + "/settings.json", serializedSettings)
320
350
  )
321
351
 
322
352
  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
+ `
@@ -60,7 +60,7 @@ it("should expose the project settings including the plugin settings", async ()
60
60
  settings: settings,
61
61
  nodeishFs: {} as any,
62
62
  })
63
- await resolved.data.loadMessages!({ settings })
63
+ await resolved.data.loadMessages!({ settings, nodeishFs: {} as any })
64
64
  await resolved.data.saveMessages!({ settings, messages: [] })
65
65
  })
66
66
 
@@ -82,6 +82,7 @@ describe("loadMessages", () => {
82
82
  expect(
83
83
  await resolved.data.loadMessages!({
84
84
  settings: {} as any,
85
+ nodeishFs: {} as any,
85
86
  })
86
87
  ).toEqual([{ id: "test", expressions: [], selectors: [], variants: [] }])
87
88
  })
@@ -91,7 +91,7 @@ export const resolvePlugins: ResolvePluginsFunction = async (args) => {
91
91
  result.data.loadMessages = (_args) =>
92
92
  plugin.loadMessages!({
93
93
  ..._args,
94
- nodeishFs: args.nodeishFs,
94
+ // renoved nodeishFs from args because we need to pass custom wrapped fs that establishes a watcher
95
95
  })
96
96
  }
97
97
 
@@ -18,7 +18,7 @@ import type { ProjectSettings } from "@inlang/project-settings"
18
18
  */
19
19
  export type NodeishFilesystemSubset = Pick<
20
20
  NodeishFilesystem,
21
- "readFile" | "readdir" | "mkdir" | "writeFile"
21
+ "readFile" | "readdir" | "mkdir" | "writeFile" | "watch"
22
22
  >
23
23
 
24
24
  /**
@@ -44,7 +44,10 @@ export type ResolvePluginsFunction = (args: {
44
44
  * The API after resolving the plugins.
45
45
  */
46
46
  export type ResolvedPluginApi = {
47
- loadMessages: (args: { settings: ProjectSettings }) => Promise<Message[]> | Message[]
47
+ loadMessages: (args: {
48
+ settings: ProjectSettings
49
+ nodeishFs: NodeishFilesystemSubset
50
+ }) => Promise<Message[]> | Message[]
48
51
  saveMessages: (args: { settings: ProjectSettings; messages: Message[] }) => Promise<void> | void
49
52
  /**
50
53
  * App specific APIs.
@@ -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"