@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.
- package/README.md +25 -0
- package/dist/adapter/solidAdapter.d.ts +32 -0
- package/dist/adapter/solidAdapter.d.ts.map +1 -0
- package/dist/adapter/solidAdapter.js +39 -0
- package/dist/adapter/solidAdapter.test.d.ts +2 -0
- package/dist/adapter/solidAdapter.test.d.ts.map +1 -0
- package/dist/adapter/solidAdapter.test.js +284 -0
- package/dist/api.d.ts +88 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +1 -0
- package/dist/createMessageLintReportsQuery.d.ts +9 -0
- package/dist/createMessageLintReportsQuery.d.ts.map +1 -0
- package/dist/createMessageLintReportsQuery.js +48 -0
- package/dist/createMessagesQuery.d.ts +7 -0
- package/dist/createMessagesQuery.d.ts.map +1 -0
- package/dist/createMessagesQuery.js +57 -0
- package/dist/createMessagesQuery.test.d.ts +2 -0
- package/dist/createMessagesQuery.test.d.ts.map +1 -0
- package/dist/createMessagesQuery.test.js +304 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/lint/index.d.ts +3 -0
- package/dist/lint/index.d.ts.map +1 -0
- package/dist/lint/index.js +2 -0
- package/dist/lint/message/errors.d.ts +7 -0
- package/dist/lint/message/errors.d.ts.map +1 -0
- package/dist/lint/message/errors.js +9 -0
- package/dist/lint/message/lintMessages.d.ts +17 -0
- package/dist/lint/message/lintMessages.d.ts.map +1 -0
- package/dist/lint/message/lintMessages.js +12 -0
- package/dist/lint/message/lintMessages.test.d.ts +2 -0
- package/dist/lint/message/lintMessages.test.d.ts.map +1 -0
- package/dist/lint/message/lintMessages.test.js +105 -0
- package/dist/lint/message/lintSingleMessage.d.ts +23 -0
- package/dist/lint/message/lintSingleMessage.d.ts.map +1 -0
- package/dist/lint/message/lintSingleMessage.js +36 -0
- package/dist/lint/message/lintSingleMessage.test.d.ts +2 -0
- package/dist/lint/message/lintSingleMessage.test.d.ts.map +1 -0
- package/dist/lint/message/lintSingleMessage.test.js +155 -0
- package/dist/messages/errors.d.ts +13 -0
- package/dist/messages/errors.d.ts.map +1 -0
- package/dist/messages/errors.js +18 -0
- package/dist/messages/index.d.ts +3 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +2 -0
- package/dist/messages/variant.d.ts +46 -0
- package/dist/messages/variant.d.ts.map +1 -0
- package/dist/messages/variant.js +177 -0
- package/dist/messages/variant.test.d.ts +2 -0
- package/dist/messages/variant.test.d.ts.map +1 -0
- package/dist/messages/variant.test.js +379 -0
- package/dist/openInlangProject.d.ts +18 -0
- package/dist/openInlangProject.d.ts.map +1 -0
- package/dist/openInlangProject.js +226 -0
- package/dist/openInlangProject.test.d.ts +2 -0
- package/dist/openInlangProject.test.d.ts.map +1 -0
- package/dist/openInlangProject.test.js +627 -0
- package/dist/parseConfig.d.ts +8 -0
- package/dist/parseConfig.d.ts.map +1 -0
- package/dist/parseConfig.js +26 -0
- package/dist/reactivity/map.d.ts +66 -0
- package/dist/reactivity/map.d.ts.map +1 -0
- package/dist/reactivity/map.js +143 -0
- package/dist/reactivity/solid.d.ts +12 -0
- package/dist/reactivity/solid.d.ts.map +1 -0
- package/dist/reactivity/solid.js +13 -0
- package/dist/reactivity/trigger.d.ts +11 -0
- package/dist/reactivity/trigger.d.ts.map +1 -0
- package/dist/reactivity/trigger.js +46 -0
- package/dist/resolve-modules/errors.d.ts +34 -0
- package/dist/resolve-modules/errors.d.ts.map +1 -0
- package/dist/resolve-modules/errors.js +35 -0
- package/dist/resolve-modules/import.d.ts +35 -0
- package/dist/resolve-modules/import.d.ts.map +1 -0
- package/dist/resolve-modules/import.js +40 -0
- package/dist/resolve-modules/import.test.d.ts +2 -0
- package/dist/resolve-modules/import.test.d.ts.map +1 -0
- package/dist/resolve-modules/import.test.js +45 -0
- package/dist/resolve-modules/index.d.ts +3 -0
- package/dist/resolve-modules/index.d.ts.map +1 -0
- package/dist/resolve-modules/index.js +2 -0
- package/dist/resolve-modules/message-lint-rules/errors.d.ts +8 -0
- package/dist/resolve-modules/message-lint-rules/errors.d.ts.map +1 -0
- package/dist/resolve-modules/message-lint-rules/errors.js +8 -0
- package/dist/resolve-modules/message-lint-rules/resolveMessageLintRules.d.ts +9 -0
- package/dist/resolve-modules/message-lint-rules/resolveMessageLintRules.d.ts.map +1 -0
- package/dist/resolve-modules/message-lint-rules/resolveMessageLintRules.js +21 -0
- package/dist/resolve-modules/plugins/errors.d.ts +28 -0
- package/dist/resolve-modules/plugins/errors.d.ts.map +1 -0
- package/dist/resolve-modules/plugins/errors.js +44 -0
- package/dist/resolve-modules/plugins/resolvePlugins.d.ts +3 -0
- package/dist/resolve-modules/plugins/resolvePlugins.d.ts.map +1 -0
- package/dist/resolve-modules/plugins/resolvePlugins.js +108 -0
- package/dist/resolve-modules/plugins/resolvePlugins.test.d.ts +2 -0
- package/dist/resolve-modules/plugins/resolvePlugins.test.d.ts.map +1 -0
- package/dist/resolve-modules/plugins/resolvePlugins.test.js +289 -0
- package/dist/resolve-modules/plugins/types.d.ts +60 -0
- package/dist/resolve-modules/plugins/types.d.ts.map +1 -0
- package/dist/resolve-modules/plugins/types.js +1 -0
- package/dist/resolve-modules/plugins/types.test.d.ts +2 -0
- package/dist/resolve-modules/plugins/types.test.d.ts.map +1 -0
- package/dist/resolve-modules/plugins/types.test.js +49 -0
- package/dist/resolve-modules/resolveModules.d.ts +3 -0
- package/dist/resolve-modules/resolveModules.d.ts.map +1 -0
- package/dist/resolve-modules/resolveModules.js +70 -0
- package/dist/resolve-modules/resolveModules.test.d.ts +2 -0
- package/dist/resolve-modules/resolveModules.test.d.ts.map +1 -0
- package/dist/resolve-modules/resolveModules.test.js +143 -0
- package/dist/resolve-modules/types.d.ts +62 -0
- package/dist/resolve-modules/types.d.ts.map +1 -0
- package/dist/resolve-modules/types.js +1 -0
- package/dist/test-utilities/createMessage.d.ts +17 -0
- package/dist/test-utilities/createMessage.d.ts.map +1 -0
- package/dist/test-utilities/createMessage.js +16 -0
- package/dist/test-utilities/createMessage.test.d.ts +2 -0
- package/dist/test-utilities/createMessage.test.d.ts.map +1 -0
- package/dist/test-utilities/createMessage.test.js +91 -0
- package/dist/test-utilities/index.d.ts +2 -0
- package/dist/test-utilities/index.d.ts.map +1 -0
- package/dist/test-utilities/index.js +1 -0
- package/dist/versionedInterfaces.d.ts +8 -0
- package/dist/versionedInterfaces.d.ts.map +1 -0
- package/dist/versionedInterfaces.js +8 -0
- package/package.json +58 -0
- package/src/adapter/solidAdapter.test.ts +363 -0
- package/src/adapter/solidAdapter.ts +77 -0
- package/src/api.ts +86 -0
- package/src/createMessageLintReportsQuery.ts +77 -0
- package/src/createMessagesQuery.test.ts +435 -0
- package/src/createMessagesQuery.ts +64 -0
- package/src/errors.ts +46 -0
- package/src/index.ts +29 -0
- package/src/lint/index.ts +2 -0
- package/src/lint/message/errors.ts +9 -0
- package/src/lint/message/lintMessages.test.ts +122 -0
- package/src/lint/message/lintMessages.ts +33 -0
- package/src/lint/message/lintSingleMessage.test.ts +183 -0
- package/src/lint/message/lintSingleMessage.ts +62 -0
- package/src/messages/errors.ts +25 -0
- package/src/messages/index.ts +2 -0
- package/src/messages/variant.test.ts +444 -0
- package/src/messages/variant.ts +242 -0
- package/src/openInlangProject.test.ts +734 -0
- package/src/openInlangProject.ts +337 -0
- package/src/parseConfig.ts +33 -0
- package/src/reactivity/map.ts +135 -0
- package/src/reactivity/solid.ts +36 -0
- package/src/reactivity/trigger.ts +46 -0
- package/src/resolve-modules/errors.ts +39 -0
- package/src/resolve-modules/import.test.ts +58 -0
- package/src/resolve-modules/import.ts +69 -0
- package/src/resolve-modules/index.ts +2 -0
- package/src/resolve-modules/message-lint-rules/errors.ts +9 -0
- package/src/resolve-modules/message-lint-rules/resolveMessageLintRules.ts +24 -0
- package/src/resolve-modules/plugins/errors.ts +57 -0
- package/src/resolve-modules/plugins/resolvePlugins.test.ts +340 -0
- package/src/resolve-modules/plugins/resolvePlugins.ts +170 -0
- package/src/resolve-modules/plugins/types.test.ts +57 -0
- package/src/resolve-modules/plugins/types.ts +77 -0
- package/src/resolve-modules/resolveModules.test.ts +176 -0
- package/src/resolve-modules/resolveModules.ts +97 -0
- package/src/resolve-modules/types.ts +71 -0
- package/src/test-utilities/createMessage.test.ts +100 -0
- package/src/test-utilities/createMessage.ts +20 -0
- package/src/test-utilities/index.ts +1 -0
- 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,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
|
+
}
|