@inlang/sdk 0.1.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 (170) hide show
  1. package/README.md +25 -0
  2. package/dist/adapter/solidAdapter.d.ts +32 -0
  3. package/dist/adapter/solidAdapter.d.ts.map +1 -0
  4. package/dist/adapter/solidAdapter.js +39 -0
  5. package/dist/adapter/solidAdapter.test.d.ts +2 -0
  6. package/dist/adapter/solidAdapter.test.d.ts.map +1 -0
  7. package/dist/adapter/solidAdapter.test.js +284 -0
  8. package/dist/api.d.ts +88 -0
  9. package/dist/api.d.ts.map +1 -0
  10. package/dist/api.js +1 -0
  11. package/dist/createMessageLintReportsQuery.d.ts +9 -0
  12. package/dist/createMessageLintReportsQuery.d.ts.map +1 -0
  13. package/dist/createMessageLintReportsQuery.js +48 -0
  14. package/dist/createMessagesQuery.d.ts +7 -0
  15. package/dist/createMessagesQuery.d.ts.map +1 -0
  16. package/dist/createMessagesQuery.js +57 -0
  17. package/dist/createMessagesQuery.test.d.ts +2 -0
  18. package/dist/createMessagesQuery.test.d.ts.map +1 -0
  19. package/dist/createMessagesQuery.test.js +304 -0
  20. package/dist/errors.d.ts +22 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.js +39 -0
  23. package/dist/index.d.ts +15 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +13 -0
  26. package/dist/lint/index.d.ts +3 -0
  27. package/dist/lint/index.d.ts.map +1 -0
  28. package/dist/lint/index.js +2 -0
  29. package/dist/lint/message/errors.d.ts +7 -0
  30. package/dist/lint/message/errors.d.ts.map +1 -0
  31. package/dist/lint/message/errors.js +9 -0
  32. package/dist/lint/message/lintMessages.d.ts +17 -0
  33. package/dist/lint/message/lintMessages.d.ts.map +1 -0
  34. package/dist/lint/message/lintMessages.js +12 -0
  35. package/dist/lint/message/lintMessages.test.d.ts +2 -0
  36. package/dist/lint/message/lintMessages.test.d.ts.map +1 -0
  37. package/dist/lint/message/lintMessages.test.js +105 -0
  38. package/dist/lint/message/lintSingleMessage.d.ts +23 -0
  39. package/dist/lint/message/lintSingleMessage.d.ts.map +1 -0
  40. package/dist/lint/message/lintSingleMessage.js +36 -0
  41. package/dist/lint/message/lintSingleMessage.test.d.ts +2 -0
  42. package/dist/lint/message/lintSingleMessage.test.d.ts.map +1 -0
  43. package/dist/lint/message/lintSingleMessage.test.js +155 -0
  44. package/dist/messages/errors.d.ts +13 -0
  45. package/dist/messages/errors.d.ts.map +1 -0
  46. package/dist/messages/errors.js +18 -0
  47. package/dist/messages/index.d.ts +3 -0
  48. package/dist/messages/index.d.ts.map +1 -0
  49. package/dist/messages/index.js +2 -0
  50. package/dist/messages/variant.d.ts +46 -0
  51. package/dist/messages/variant.d.ts.map +1 -0
  52. package/dist/messages/variant.js +177 -0
  53. package/dist/messages/variant.test.d.ts +2 -0
  54. package/dist/messages/variant.test.d.ts.map +1 -0
  55. package/dist/messages/variant.test.js +379 -0
  56. package/dist/openInlangProject.d.ts +18 -0
  57. package/dist/openInlangProject.d.ts.map +1 -0
  58. package/dist/openInlangProject.js +226 -0
  59. package/dist/openInlangProject.test.d.ts +2 -0
  60. package/dist/openInlangProject.test.d.ts.map +1 -0
  61. package/dist/openInlangProject.test.js +627 -0
  62. package/dist/parseConfig.d.ts +8 -0
  63. package/dist/parseConfig.d.ts.map +1 -0
  64. package/dist/parseConfig.js +26 -0
  65. package/dist/reactivity/map.d.ts +66 -0
  66. package/dist/reactivity/map.d.ts.map +1 -0
  67. package/dist/reactivity/map.js +143 -0
  68. package/dist/reactivity/solid.d.ts +12 -0
  69. package/dist/reactivity/solid.d.ts.map +1 -0
  70. package/dist/reactivity/solid.js +13 -0
  71. package/dist/reactivity/trigger.d.ts +11 -0
  72. package/dist/reactivity/trigger.d.ts.map +1 -0
  73. package/dist/reactivity/trigger.js +46 -0
  74. package/dist/resolve-modules/errors.d.ts +34 -0
  75. package/dist/resolve-modules/errors.d.ts.map +1 -0
  76. package/dist/resolve-modules/errors.js +35 -0
  77. package/dist/resolve-modules/import.d.ts +35 -0
  78. package/dist/resolve-modules/import.d.ts.map +1 -0
  79. package/dist/resolve-modules/import.js +40 -0
  80. package/dist/resolve-modules/import.test.d.ts +2 -0
  81. package/dist/resolve-modules/import.test.d.ts.map +1 -0
  82. package/dist/resolve-modules/import.test.js +45 -0
  83. package/dist/resolve-modules/index.d.ts +3 -0
  84. package/dist/resolve-modules/index.d.ts.map +1 -0
  85. package/dist/resolve-modules/index.js +2 -0
  86. package/dist/resolve-modules/message-lint-rules/errors.d.ts +8 -0
  87. package/dist/resolve-modules/message-lint-rules/errors.d.ts.map +1 -0
  88. package/dist/resolve-modules/message-lint-rules/errors.js +8 -0
  89. package/dist/resolve-modules/message-lint-rules/resolveMessageLintRules.d.ts +9 -0
  90. package/dist/resolve-modules/message-lint-rules/resolveMessageLintRules.d.ts.map +1 -0
  91. package/dist/resolve-modules/message-lint-rules/resolveMessageLintRules.js +21 -0
  92. package/dist/resolve-modules/plugins/errors.d.ts +28 -0
  93. package/dist/resolve-modules/plugins/errors.d.ts.map +1 -0
  94. package/dist/resolve-modules/plugins/errors.js +44 -0
  95. package/dist/resolve-modules/plugins/resolvePlugins.d.ts +3 -0
  96. package/dist/resolve-modules/plugins/resolvePlugins.d.ts.map +1 -0
  97. package/dist/resolve-modules/plugins/resolvePlugins.js +108 -0
  98. package/dist/resolve-modules/plugins/resolvePlugins.test.d.ts +2 -0
  99. package/dist/resolve-modules/plugins/resolvePlugins.test.d.ts.map +1 -0
  100. package/dist/resolve-modules/plugins/resolvePlugins.test.js +289 -0
  101. package/dist/resolve-modules/plugins/types.d.ts +60 -0
  102. package/dist/resolve-modules/plugins/types.d.ts.map +1 -0
  103. package/dist/resolve-modules/plugins/types.js +1 -0
  104. package/dist/resolve-modules/plugins/types.test.d.ts +2 -0
  105. package/dist/resolve-modules/plugins/types.test.d.ts.map +1 -0
  106. package/dist/resolve-modules/plugins/types.test.js +49 -0
  107. package/dist/resolve-modules/resolveModules.d.ts +3 -0
  108. package/dist/resolve-modules/resolveModules.d.ts.map +1 -0
  109. package/dist/resolve-modules/resolveModules.js +70 -0
  110. package/dist/resolve-modules/resolveModules.test.d.ts +2 -0
  111. package/dist/resolve-modules/resolveModules.test.d.ts.map +1 -0
  112. package/dist/resolve-modules/resolveModules.test.js +143 -0
  113. package/dist/resolve-modules/types.d.ts +62 -0
  114. package/dist/resolve-modules/types.d.ts.map +1 -0
  115. package/dist/resolve-modules/types.js +1 -0
  116. package/dist/test-utilities/createMessage.d.ts +17 -0
  117. package/dist/test-utilities/createMessage.d.ts.map +1 -0
  118. package/dist/test-utilities/createMessage.js +16 -0
  119. package/dist/test-utilities/createMessage.test.d.ts +2 -0
  120. package/dist/test-utilities/createMessage.test.d.ts.map +1 -0
  121. package/dist/test-utilities/createMessage.test.js +91 -0
  122. package/dist/test-utilities/index.d.ts +2 -0
  123. package/dist/test-utilities/index.d.ts.map +1 -0
  124. package/dist/test-utilities/index.js +1 -0
  125. package/dist/versionedInterfaces.d.ts +8 -0
  126. package/dist/versionedInterfaces.d.ts.map +1 -0
  127. package/dist/versionedInterfaces.js +8 -0
  128. package/package.json +58 -0
  129. package/src/adapter/solidAdapter.test.ts +363 -0
  130. package/src/adapter/solidAdapter.ts +77 -0
  131. package/src/api.ts +86 -0
  132. package/src/createMessageLintReportsQuery.ts +77 -0
  133. package/src/createMessagesQuery.test.ts +435 -0
  134. package/src/createMessagesQuery.ts +64 -0
  135. package/src/errors.ts +46 -0
  136. package/src/index.ts +29 -0
  137. package/src/lint/index.ts +2 -0
  138. package/src/lint/message/errors.ts +9 -0
  139. package/src/lint/message/lintMessages.test.ts +122 -0
  140. package/src/lint/message/lintMessages.ts +33 -0
  141. package/src/lint/message/lintSingleMessage.test.ts +183 -0
  142. package/src/lint/message/lintSingleMessage.ts +62 -0
  143. package/src/messages/errors.ts +25 -0
  144. package/src/messages/index.ts +2 -0
  145. package/src/messages/variant.test.ts +444 -0
  146. package/src/messages/variant.ts +242 -0
  147. package/src/openInlangProject.test.ts +734 -0
  148. package/src/openInlangProject.ts +337 -0
  149. package/src/parseConfig.ts +33 -0
  150. package/src/reactivity/map.ts +135 -0
  151. package/src/reactivity/solid.ts +36 -0
  152. package/src/reactivity/trigger.ts +46 -0
  153. package/src/resolve-modules/errors.ts +39 -0
  154. package/src/resolve-modules/import.test.ts +58 -0
  155. package/src/resolve-modules/import.ts +69 -0
  156. package/src/resolve-modules/index.ts +2 -0
  157. package/src/resolve-modules/message-lint-rules/errors.ts +9 -0
  158. package/src/resolve-modules/message-lint-rules/resolveMessageLintRules.ts +24 -0
  159. package/src/resolve-modules/plugins/errors.ts +57 -0
  160. package/src/resolve-modules/plugins/resolvePlugins.test.ts +340 -0
  161. package/src/resolve-modules/plugins/resolvePlugins.ts +170 -0
  162. package/src/resolve-modules/plugins/types.test.ts +57 -0
  163. package/src/resolve-modules/plugins/types.ts +77 -0
  164. package/src/resolve-modules/resolveModules.test.ts +176 -0
  165. package/src/resolve-modules/resolveModules.ts +97 -0
  166. package/src/resolve-modules/types.ts +71 -0
  167. package/src/test-utilities/createMessage.test.ts +100 -0
  168. package/src/test-utilities/createMessage.ts +20 -0
  169. package/src/test-utilities/index.ts +1 -0
  170. package/src/versionedInterfaces.ts +9 -0
@@ -0,0 +1,69 @@
1
+ import { dedent } from "ts-dedent"
2
+ import { normalizePath } from "@lix-js/fs"
3
+ import type { NodeishFilesystemSubset } from "@inlang/plugin"
4
+ import { ModuleImportError } from "./errors.js"
5
+
6
+ /**
7
+ * Importing ES modules either from a local path, or from a url.
8
+ *
9
+ * - Name the import function `_import` to avoid shadowing the
10
+ * native import function.
11
+ */
12
+ export type ImportFunction = (uri: string) => Promise<any>
13
+
14
+ /**
15
+ * Creates the import function.
16
+ *
17
+ * This function is required to import modules from a local path.
18
+ *
19
+ * @example
20
+ * const $import = createImport({ readFile: fs.readFile, fetch });
21
+ * const module = await _import('./some-module.js');
22
+ */
23
+ export function createImport(args: {
24
+ /** the fs from which the file can be read */
25
+ readFile: NodeishFilesystemSubset["readFile"]
26
+ /** http client implementation */
27
+ fetch: typeof fetch
28
+ }): (uri: string) => ReturnType<typeof $import> {
29
+ // resembles a native import api
30
+ return (uri: string) => $import(uri, args)
31
+ }
32
+
33
+ async function $import(
34
+ uri: string,
35
+ options: {
36
+ /**
37
+ * Required to import from a local path.
38
+ */
39
+ readFile: NodeishFilesystemSubset["readFile"]
40
+ /**
41
+ * Required to import via network.
42
+ */
43
+ fetch: typeof fetch
44
+ },
45
+ ): Promise<any> {
46
+ let moduleAsText: string
47
+
48
+ if (uri.startsWith("http")) {
49
+ moduleAsText = await (await options.fetch(uri)).text()
50
+ } else {
51
+ moduleAsText = await options.readFile(normalizePath(uri), { encoding: "utf-8" })
52
+ }
53
+
54
+ const moduleWithMimeType = "data:application/javascript," + encodeURIComponent(moduleAsText)
55
+
56
+ try {
57
+ return await import(/* @vite-ignore */ moduleWithMimeType)
58
+ } catch (error) {
59
+ let message = `Error while importing ${uri}: ${(error as Error)?.message ?? "Unknown error"}`
60
+ if (error instanceof SyntaxError && uri.includes("jsdelivr")) {
61
+ message += dedent`\n\n
62
+ Are you sure that the file exists on JSDelivr?
63
+
64
+ The error indicates that the imported file does not exist on JSDelivr. For non-existent files, JSDelivr returns a 404 text that JS cannot parse as a module and throws a SyntaxError.
65
+ `
66
+ }
67
+ throw new ModuleImportError(message, { module: uri, cause: error as Error })
68
+ }
69
+ }
@@ -0,0 +1,2 @@
1
+ export { resolveModules } from "./resolveModules.js"
2
+ export { type ImportFunction, createImport } from "./import.js"
@@ -0,0 +1,9 @@
1
+ export class MessageLintRuleIsInvalidError extends Error {
2
+ public readonly module: string
3
+
4
+ constructor(message: string, options: { module: string; cause?: Error }) {
5
+ super(message)
6
+ this.module = options.module
7
+ this.name = "MessageLintRuleIsInvalidError"
8
+ }
9
+ }
@@ -0,0 +1,24 @@
1
+ import { Value } from "@sinclair/typebox/value"
2
+ import { MessageLintRule } from "@inlang/message-lint-rule"
3
+ import { MessageLintRuleIsInvalidError } from "./errors.js"
4
+
5
+ export const resolveMessageLintRules = (args: { messageLintRules: Array<MessageLintRule> }) => {
6
+ const result = {
7
+ data: [] as Array<MessageLintRule>,
8
+ errors: [] as MessageLintRuleIsInvalidError[],
9
+ }
10
+ for (const rule of args.messageLintRules) {
11
+ if (Value.Check(MessageLintRule, rule) === false) {
12
+ result.errors.push(
13
+ new MessageLintRuleIsInvalidError(`Couldn't parse lint rule "${rule.meta.id}"`, {
14
+ module: "not implemented",
15
+ }),
16
+ )
17
+ continue
18
+ } else {
19
+ result.data.push(rule)
20
+ }
21
+ }
22
+
23
+ return result
24
+ }
@@ -0,0 +1,57 @@
1
+ import type { Plugin } from "@inlang/plugin"
2
+
3
+ type PluginErrorOptions = {
4
+ plugin: Plugin["meta"]["id"]
5
+ } & Partial<Error>
6
+
7
+ class PluginError extends Error {
8
+ public readonly plugin: string
9
+
10
+ constructor(message: string, options: PluginErrorOptions) {
11
+ super(message)
12
+ this.name = "PluginError"
13
+ this.plugin = options.plugin
14
+ }
15
+ }
16
+
17
+ export class PluginHasInvalidIdError extends PluginError {
18
+ constructor(message: string, options: PluginErrorOptions) {
19
+ super(message, options)
20
+ this.name = "PluginHasInvalidIdError"
21
+ }
22
+ }
23
+
24
+ export class PluginUsesReservedNamespaceError extends PluginError {
25
+ constructor(message: string, options: PluginErrorOptions) {
26
+ super(message, options)
27
+ this.name = "PluginUsesReservedNamespaceError"
28
+ }
29
+ }
30
+
31
+ export class PluginHasInvalidSchemaError extends PluginError {
32
+ constructor(message: string, options: PluginErrorOptions) {
33
+ super(message, options)
34
+ this.name = "PluginHasInvalidSchemaError"
35
+ }
36
+ }
37
+
38
+ export class PluginLoadMessagesFunctionAlreadyDefinedError extends PluginError {
39
+ constructor(message: string, options: PluginErrorOptions) {
40
+ super(message, options)
41
+ this.name = "PluginLoadMessagesFunctionAlreadyDefinedError"
42
+ }
43
+ }
44
+
45
+ export class PluginSaveMessagesFunctionAlreadyDefinedError extends PluginError {
46
+ constructor(message: string, options: PluginErrorOptions) {
47
+ super(message, options)
48
+ this.name = "PluginSaveMessagesFunctionAlreadyDefinedError"
49
+ }
50
+ }
51
+
52
+ export class PluginReturnedInvalidCustomApiError extends PluginError {
53
+ constructor(message: string, options: PluginErrorOptions) {
54
+ super(message, options)
55
+ this.name = "PluginReturnedInvalidCustomApiError"
56
+ }
57
+ }
@@ -0,0 +1,340 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+ import { describe, expect, it } from "vitest"
3
+ import { resolvePlugins } from "./resolvePlugins.js"
4
+ import {
5
+ PluginLoadMessagesFunctionAlreadyDefinedError,
6
+ PluginSaveMessagesFunctionAlreadyDefinedError,
7
+ PluginHasInvalidIdError,
8
+ PluginUsesReservedNamespaceError,
9
+ PluginReturnedInvalidCustomApiError,
10
+ PluginHasInvalidSchemaError,
11
+ } from "./errors.js"
12
+ import type { Plugin } from "@inlang/plugin"
13
+
14
+ describe("generally", () => {
15
+ it("should return an error if a plugin uses an invalid id", async () => {
16
+ const mockPlugin: Plugin = {
17
+ meta: {
18
+ // @ts-expect-error - invalid id
19
+ id: "no-namespace",
20
+ description: { en: "My plugin description" },
21
+ displayName: { en: "My plugin" },
22
+ },
23
+ loadMessages: () => undefined as any,
24
+ saveMessages: () => undefined as any,
25
+ addCustomApi() {
26
+ return {}
27
+ },
28
+ }
29
+
30
+ const resolved = await resolvePlugins({
31
+ plugins: [mockPlugin],
32
+ settings: {},
33
+ nodeishFs: {} as any,
34
+ })
35
+
36
+ expect(resolved.errors[0]).toBeInstanceOf(PluginHasInvalidIdError)
37
+ })
38
+
39
+ it("should return an error if a plugin uses APIs that are not available", async () => {
40
+ const mockPlugin: Plugin = {
41
+ meta: {
42
+ id: "plugin.namespace.undefinedApi",
43
+ description: { en: "My plugin description" },
44
+ displayName: { en: "My plugin" },
45
+ },
46
+ // @ts-expect-error the key is not available in type
47
+ nonExistentKey: {
48
+ nonexistentOptions: "value",
49
+ },
50
+ loadMessages: () => undefined as any,
51
+ saveMessages: () => undefined as any,
52
+ }
53
+
54
+ const resolved = await resolvePlugins({
55
+ plugins: [mockPlugin],
56
+ settings: {},
57
+ nodeishFs: {} as any,
58
+ })
59
+
60
+ expect(resolved.errors.length).toBe(1)
61
+ expect(resolved.errors[0]).toBeInstanceOf(PluginHasInvalidSchemaError)
62
+ })
63
+
64
+ it("should not initialize a plugin that uses the 'inlang' namespace except for inlang whitelisted plugins", async () => {
65
+ const mockPlugin: Plugin = {
66
+ meta: {
67
+ id: "plugin.inlang.notWhitelisted",
68
+ description: { en: "My plugin description" },
69
+ displayName: { en: "My plugin" },
70
+ },
71
+ loadMessages: () => undefined as any,
72
+ }
73
+
74
+ const resolved = await resolvePlugins({
75
+ plugins: [mockPlugin],
76
+ settings: {},
77
+ nodeishFs: {} as any,
78
+ })
79
+
80
+ expect(resolved.errors.length).toBe(1)
81
+ expect(resolved.errors[0]).toBeInstanceOf(PluginUsesReservedNamespaceError)
82
+ })
83
+ })
84
+
85
+ describe("loadMessages", () => {
86
+ it("should load messages from a local source", async () => {
87
+ const mockPlugin: Plugin = {
88
+ meta: {
89
+ id: "plugin.namespace.placeholder",
90
+ description: { en: "My plugin description" },
91
+ displayName: { en: "My plugin" },
92
+ },
93
+ loadMessages: async () => [{ id: "test", expressions: [], selectors: [], variants: [] }],
94
+ }
95
+
96
+ const resolved = await resolvePlugins({
97
+ plugins: [mockPlugin],
98
+ settings: {},
99
+ nodeishFs: {} as any,
100
+ })
101
+
102
+ expect(
103
+ await resolved.data.loadMessages!({
104
+ languageTags: ["en"],
105
+ sourceLanguageTag: "en",
106
+ }),
107
+ ).toEqual([{ id: "test", expressions: [], selectors: [], variants: [] }])
108
+ })
109
+
110
+ it("should collect an error if function is defined twice in multiple plugins", async () => {
111
+ const mockPlugin: Plugin = {
112
+ meta: {
113
+ id: "plugin.namepsace.loadMessagesFirst",
114
+ description: { en: "My plugin description" },
115
+ displayName: { en: "My plugin" },
116
+ },
117
+ loadMessages: async () => undefined as any,
118
+ }
119
+ const mockPlugin2: Plugin = {
120
+ meta: {
121
+ id: "plugin.namepsace.loadMessagesSecond",
122
+ description: { en: "My plugin description" },
123
+ displayName: { en: "My plugin" },
124
+ },
125
+ loadMessages: async () => undefined as any,
126
+ }
127
+
128
+ const resolved = await resolvePlugins({
129
+ plugins: [mockPlugin, mockPlugin2],
130
+ nodeishFs: {} as any,
131
+ settings: {},
132
+ })
133
+
134
+ expect(resolved.errors).toHaveLength(1)
135
+ expect(resolved.errors[0]).toBeInstanceOf(PluginLoadMessagesFunctionAlreadyDefinedError)
136
+ })
137
+ })
138
+
139
+ describe("saveMessages", () => {
140
+ it("should save messages to a local source", async () => {
141
+ const mockPlugin: Plugin = {
142
+ meta: {
143
+ id: "plugin.namespace.placeholder",
144
+ description: { en: "My plugin description" },
145
+ displayName: { en: "My plugin" },
146
+ },
147
+ saveMessages: async () => undefined as any,
148
+ }
149
+
150
+ const resolved = await resolvePlugins({
151
+ plugins: [mockPlugin],
152
+ nodeishFs: {} as any,
153
+ settings: {},
154
+ })
155
+
156
+ expect(resolved.errors).toHaveLength(0)
157
+ })
158
+
159
+ it("should collect an error if function is defined twice in multiple plugins", async () => {
160
+ const mockPlugin: Plugin = {
161
+ meta: {
162
+ id: "plugin.namepsace.saveMessages",
163
+ description: { en: "My plugin description" },
164
+ displayName: { en: "My plugin" },
165
+ },
166
+ saveMessages: async () => undefined as any,
167
+ }
168
+ const mockPlugin2: Plugin = {
169
+ meta: {
170
+ id: "plugin.namepsace.saveMessages2",
171
+ description: { en: "My plugin description" },
172
+ displayName: { en: "My plugin" },
173
+ },
174
+
175
+ saveMessages: async () => undefined as any,
176
+ }
177
+
178
+ const resolved = await resolvePlugins({
179
+ plugins: [mockPlugin, mockPlugin2],
180
+ settings: {},
181
+ nodeishFs: {} as any,
182
+ })
183
+
184
+ expect(resolved.errors).toHaveLength(1)
185
+ expect(resolved.errors[0]).toBeInstanceOf(PluginSaveMessagesFunctionAlreadyDefinedError)
186
+ })
187
+ })
188
+
189
+ describe("detectedLanguageTags", () => {
190
+ it("should merge language tags from plugins", async () => {
191
+ const mockPlugin: Plugin = {
192
+ meta: {
193
+ id: "plugin.namepsace.detectedLanguageTags",
194
+ description: { en: "My plugin description" },
195
+ displayName: { en: "My plugin" },
196
+ },
197
+ detectedLanguageTags: async () => ["de", "en"],
198
+ addCustomApi: () => {
199
+ return {}
200
+ },
201
+ }
202
+ const mockPlugin2: Plugin = {
203
+ meta: {
204
+ id: "plugin.namepsace.detectedLanguageTags2",
205
+ description: { en: "My plugin description" },
206
+ displayName: { en: "My plugin" },
207
+ },
208
+ addCustomApi: () => {
209
+ return {}
210
+ },
211
+ detectedLanguageTags: async () => ["de", "fr"],
212
+ }
213
+
214
+ const resolved = await resolvePlugins({
215
+ plugins: [mockPlugin, mockPlugin2],
216
+ settings: {},
217
+ nodeishFs: {} as any,
218
+ })
219
+
220
+ expect(resolved.data.detectedLanguageTags).toEqual(["de", "en", "fr"])
221
+ })
222
+ })
223
+
224
+ describe("addCustomApi", () => {
225
+ it("it should resolve app specific api", async () => {
226
+ const mockPlugin: Plugin = {
227
+ meta: {
228
+ id: "plugin.namespace.placeholder",
229
+ description: { en: "My plugin description" },
230
+ displayName: { en: "My plugin" },
231
+ },
232
+
233
+ addCustomApi: () => ({
234
+ "my-app": {
235
+ messageReferenceMatcher: () => undefined as any,
236
+ },
237
+ }),
238
+ }
239
+
240
+ const resolved = await resolvePlugins({
241
+ plugins: [mockPlugin],
242
+ settings: {},
243
+ nodeishFs: {} as any,
244
+ })
245
+
246
+ expect(resolved.data.customApi).toHaveProperty("my-app")
247
+ })
248
+
249
+ it("it should resolve multiple app specific apis", async () => {
250
+ const mockPlugin: Plugin = {
251
+ meta: {
252
+ id: "plugin.namespace.placeholder",
253
+ description: { en: "My plugin description" },
254
+ displayName: { en: "My plugin" },
255
+ },
256
+ addCustomApi: () => ({
257
+ "my-app-1": {
258
+ functionOfMyApp1: () => undefined as any,
259
+ },
260
+ "my-app-2": {
261
+ functionOfMyApp2: () => undefined as any,
262
+ },
263
+ }),
264
+ }
265
+ const mockPlugin2: Plugin = {
266
+ meta: {
267
+ id: "plugin.namespace.placeholder2",
268
+ description: { en: "My plugin description" },
269
+ displayName: { en: "My plugin" },
270
+ },
271
+
272
+ addCustomApi: () => ({
273
+ "my-app-3": {
274
+ functionOfMyApp3: () => undefined as any,
275
+ },
276
+ }),
277
+ }
278
+
279
+ const resolved = await resolvePlugins({
280
+ plugins: [mockPlugin, mockPlugin2],
281
+ settings: {},
282
+ nodeishFs: {} as any,
283
+ })
284
+
285
+ expect(resolved.data.customApi).toHaveProperty("my-app-1")
286
+ expect(resolved.data.customApi).toHaveProperty("my-app-2")
287
+ expect(resolved.data.customApi).toHaveProperty("my-app-3")
288
+ })
289
+
290
+ it("it should throw an error if return value is not an object", async () => {
291
+ const mockPlugin: Plugin = {
292
+ meta: {
293
+ id: "plugin.namespace.placeholder",
294
+ description: { en: "My plugin description" },
295
+ displayName: { en: "My plugin" },
296
+ },
297
+ // @ts-expect-error - invalid return type
298
+ addCustomApi: () => undefined,
299
+ }
300
+
301
+ const resolved = await resolvePlugins({
302
+ plugins: [mockPlugin],
303
+ settings: {},
304
+ nodeishFs: {} as any,
305
+ })
306
+
307
+ expect(resolved.errors).toHaveLength(1)
308
+ expect(resolved.errors[0]).toBeInstanceOf(PluginReturnedInvalidCustomApiError)
309
+ })
310
+
311
+ it("it should throw an error if the passed options are not defined inside customApi", async () => {
312
+ const mockPlugin: Plugin = {
313
+ meta: {
314
+ id: "plugin.namepsace.placeholder",
315
+ description: { en: "My plugin description" },
316
+ displayName: { en: "My plugin" },
317
+ },
318
+ addCustomApi: () => ({
319
+ "app.inlang.placeholder": {
320
+ messageReferenceMatcher: () => {
321
+ return { hello: "world" }
322
+ },
323
+ },
324
+ }),
325
+ }
326
+
327
+ const resolved = await resolvePlugins({
328
+ plugins: [mockPlugin],
329
+ settings: {},
330
+ nodeishFs: {} as any,
331
+ })
332
+
333
+ expect(resolved.data.customApi).toHaveProperty("app.inlang.placeholder")
334
+ expect(
335
+ (resolved.data.customApi?.["app.inlang.placeholder"] as any).messageReferenceMatcher(),
336
+ ).toEqual({
337
+ hello: "world",
338
+ })
339
+ })
340
+ })
@@ -0,0 +1,170 @@
1
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
2
+ import type { ResolvePluginsFunction } from "./types.js"
3
+ import { Plugin } from "@inlang/plugin"
4
+ import {
5
+ PluginReturnedInvalidCustomApiError,
6
+ PluginLoadMessagesFunctionAlreadyDefinedError,
7
+ PluginSaveMessagesFunctionAlreadyDefinedError,
8
+ PluginHasInvalidIdError,
9
+ PluginHasInvalidSchemaError,
10
+ PluginUsesReservedNamespaceError,
11
+ } from "./errors.js"
12
+ import { deepmerge } from "deepmerge-ts"
13
+ import { TypeCompiler } from "@sinclair/typebox/compiler"
14
+ import { tryCatch } from "@inlang/result"
15
+
16
+ const whitelistedPlugins = [
17
+ "plugin.inlang.json",
18
+ "plugin.inlang.i18next",
19
+ "plugin.inlang.paraglideJs",
20
+ ]
21
+ // @ts-ignore - type mismatch error
22
+ const PluginCompiler = TypeCompiler.Compile(Plugin)
23
+
24
+ export const resolvePlugins: ResolvePluginsFunction = async (args) => {
25
+ const result: Awaited<ReturnType<ResolvePluginsFunction>> = {
26
+ data: {
27
+ loadMessages: undefined as any,
28
+ saveMessages: undefined as any,
29
+ detectedLanguageTags: [],
30
+ customApi: {},
31
+ },
32
+ errors: [],
33
+ }
34
+
35
+ for (const plugin of args.plugins) {
36
+ const errors = [...PluginCompiler.Errors(plugin)]
37
+
38
+ /**
39
+ * -------------- RESOLVE PLUGIN --------------
40
+ */
41
+
42
+ // -- INVALID ID in META --
43
+ const hasInvalidId = errors.some((error) => error.path === "/meta/id")
44
+ if (hasInvalidId) {
45
+ result.errors.push(
46
+ new PluginHasInvalidIdError(
47
+ `Plugin ${plugin.meta.id} has an invalid id "${plugin.meta.id}". It must be kebap-case and contain a namespace like project.my-plugin.`,
48
+ { plugin: plugin.meta.id },
49
+ ),
50
+ )
51
+ }
52
+
53
+ // -- USES RESERVED NAMESPACE --
54
+ if (plugin.meta.id.includes("inlang") && !whitelistedPlugins.includes(plugin.meta.id)) {
55
+ result.errors.push(
56
+ new PluginUsesReservedNamespaceError(
57
+ `Plugin ${plugin.meta.id} uses reserved namespace 'inlang'.`,
58
+ {
59
+ plugin: plugin.meta.id,
60
+ },
61
+ ),
62
+ )
63
+ }
64
+
65
+ // -- USES INVALID SCHEMA --
66
+ if (errors.length > 0) {
67
+ result.errors.push(
68
+ new PluginHasInvalidSchemaError(
69
+ `Plugin ${plugin.meta.id} uses an invalid schema. Please check the documentation for the correct Plugin type.`,
70
+ {
71
+ plugin: plugin.meta.id,
72
+ cause: errors,
73
+ },
74
+ ),
75
+ )
76
+ }
77
+
78
+ // -- ALREADY DEFINED LOADMESSAGES / SAVEMESSAGES / DETECTEDLANGUAGETAGS --
79
+ if (typeof plugin.loadMessages === "function" && result.data.loadMessages !== undefined) {
80
+ result.errors.push(
81
+ new PluginLoadMessagesFunctionAlreadyDefinedError(
82
+ `Plugin ${plugin.meta.id} defines the loadMessages function, but it was already defined by another plugin.`,
83
+ { plugin: plugin.meta.id },
84
+ ),
85
+ )
86
+ }
87
+
88
+ if (typeof plugin.saveMessages === "function" && result.data.saveMessages !== undefined) {
89
+ result.errors.push(
90
+ new PluginSaveMessagesFunctionAlreadyDefinedError(
91
+ `Plugin ${plugin.meta.id} defines the saveMessages function, but it was already defined by another plugin.`,
92
+ { plugin: plugin.meta.id },
93
+ ),
94
+ )
95
+ }
96
+
97
+ // --- ADD APP SPECIFIC API ---
98
+ if (typeof plugin.addCustomApi === "function") {
99
+ // TODO: why do we call this function 2 times (here for validation and later for retrieving the actual value)?
100
+ const { data: customApi, error } = tryCatch(() =>
101
+ plugin.addCustomApi!({
102
+ settings: args.settings?.[plugin.meta.id] ?? {},
103
+ }),
104
+ )
105
+ if (error) {
106
+ // @ts-ignore
107
+ delete error.stack
108
+ result.errors.push(error as any) // TODO: add correct error type
109
+ }
110
+ if (typeof customApi !== "object") {
111
+ result.errors.push(
112
+ new PluginReturnedInvalidCustomApiError(
113
+ `Plugin ${plugin.meta.id} defines the addCustomApi function, but it does not return an object.`,
114
+ { plugin: plugin.meta.id, cause: error },
115
+ ),
116
+ )
117
+ }
118
+ }
119
+
120
+ // -- CONTINUE IF ERRORS --
121
+ if (result.errors.length > 0) {
122
+ continue
123
+ }
124
+
125
+ /**
126
+ * -------------- BEGIN ADDING TO RESULT --------------
127
+ */
128
+
129
+ if (typeof plugin.loadMessages === "function") {
130
+ result.data.loadMessages = (_args) =>
131
+ plugin.loadMessages!({
132
+ ..._args,
133
+ settings: args.settings?.[plugin.meta.id] ?? {},
134
+ nodeishFs: args.nodeishFs,
135
+ })
136
+ }
137
+
138
+ if (typeof plugin.saveMessages === "function") {
139
+ result.data.saveMessages = (_args) =>
140
+ plugin.saveMessages!({
141
+ ..._args,
142
+ settings: args.settings?.[plugin.meta.id] ?? {},
143
+ nodeishFs: args.nodeishFs,
144
+ })
145
+ }
146
+
147
+ if (typeof plugin.detectedLanguageTags === "function") {
148
+ const detectedLangugeTags = await plugin.detectedLanguageTags!({
149
+ settings: args.settings?.[plugin.meta.id] ?? {},
150
+ nodeishFs: args.nodeishFs,
151
+ })
152
+ result.data.detectedLanguageTags = [
153
+ ...new Set([...result.data.detectedLanguageTags, ...detectedLangugeTags]),
154
+ ]
155
+ }
156
+
157
+ if (typeof plugin.addCustomApi === "function") {
158
+ const { data: customApi } = tryCatch(() =>
159
+ plugin.addCustomApi!({
160
+ settings: args.settings?.[plugin.meta.id] ?? {},
161
+ }),
162
+ )
163
+ if (customApi) {
164
+ result.data.customApi = deepmerge(result.data.customApi, customApi)
165
+ }
166
+ }
167
+ }
168
+
169
+ return result
170
+ }