@inlang/sdk 0.15.0 → 0.17.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/dist/resolve-modules/errors.d.ts +6 -0
- package/dist/resolve-modules/errors.d.ts.map +1 -1
- package/dist/resolve-modules/errors.js +9 -1
- package/dist/resolve-modules/plugins/resolvePlugins.test.js +1 -20
- package/dist/resolve-modules/resolveModules.d.ts.map +1 -1
- package/dist/resolve-modules/resolveModules.js +13 -2
- package/dist/resolve-modules/resolveModules.test.js +26 -1
- package/dist/resolve-modules/validateModuleSettings.test.d.ts +2 -0
- package/dist/resolve-modules/validateModuleSettings.test.d.ts.map +1 -0
- package/dist/resolve-modules/validateModuleSettings.test.js +71 -0
- package/dist/resolve-modules/validatedModuleSettings.d.ts +7 -0
- package/dist/resolve-modules/validatedModuleSettings.d.ts.map +1 -0
- package/dist/resolve-modules/validatedModuleSettings.js +11 -0
- package/package.json +1 -1
- package/src/adapter/solidAdapter.test.ts +3 -0
- package/src/lint/message/lintMessages.test.ts +2 -0
- package/src/lint/message/lintSingleMessage.test.ts +2 -0
- package/src/loadProject.test.ts +6 -0
- package/src/resolve-modules/errors.ts +21 -1
- package/src/resolve-modules/plugins/resolvePlugins.test.ts +4 -26
- package/src/resolve-modules/resolveModules.test.ts +31 -0
- package/src/resolve-modules/resolveModules.ts +24 -4
- package/src/resolve-modules/validateModuleSettings.test.ts +85 -0
- package/src/resolve-modules/validatedModuleSettings.ts +19 -0
|
@@ -32,4 +32,10 @@ export declare class ModuleExportIsInvalidError extends ModuleError {
|
|
|
32
32
|
errors: ValueError[];
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
+
export declare class ModuleSettingsAreInvalidError extends ModuleError {
|
|
36
|
+
constructor(options: {
|
|
37
|
+
module: string;
|
|
38
|
+
errors: ValueError[];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
35
41
|
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAC1D,cAAc,qBAAqB,CAAA;AACnC,cAAc,gCAAgC,CAAA;AAE9C,qBAAa,WAAY,SAAQ,KAAK;IACrC,SAAgB,MAAM,EAAE,MAAM,CAAA;gBAElB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAMvE;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,WAAW;gBAC3C,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAOtD;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;gBACrC,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE;CAIrD;AAED,qBAAa,0BAA2B,SAAQ,WAAW;gBAC9C,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAC1D,cAAc,qBAAqB,CAAA;AACnC,cAAc,gCAAgC,CAAA;AAE9C,qBAAa,WAAY,SAAQ,KAAK;IACrC,SAAgB,MAAM,EAAE,MAAM,CAAA;gBAElB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAMvE;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,WAAW;gBAC3C,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAOtD;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;gBACrC,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE;CAIrD;AAED,qBAAa,0BAA2B,SAAQ,WAAW;gBAC9C,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;CAY7D;AAED,qBAAa,6BAA8B,SAAQ,WAAW;gBACjD,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;CAc7D"}
|
|
@@ -30,8 +30,16 @@ export class ModuleImportError extends ModuleError {
|
|
|
30
30
|
export class ModuleExportIsInvalidError extends ModuleError {
|
|
31
31
|
constructor(options) {
|
|
32
32
|
super(`The export(s) of "${options.module}" are invalid:\n\n${options.errors
|
|
33
|
-
.map((error) => `"${error.path}" "${error.value}": "${error.message}"`)
|
|
33
|
+
.map((error) => `"${error.path}" "${JSON.stringify(error.value, undefined, 2)}": "${error.message}"`)
|
|
34
34
|
.join("\n")}`, options);
|
|
35
35
|
this.name = "ModuleExportIsInvalidError";
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
export class ModuleSettingsAreInvalidError extends ModuleError {
|
|
39
|
+
constructor(options) {
|
|
40
|
+
super(`The settings are invalid of "${options.module}" are invalid:\n\n${options.errors
|
|
41
|
+
.map((error) => `Path "${error.path}" with value "${JSON.stringify(error.value, undefined, 2)}": "${error.message}"`)
|
|
42
|
+
.join("\n")}`, options);
|
|
43
|
+
this.name = "ModuleSettingsAreInvalidError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import { resolvePlugins } from "./resolvePlugins.js";
|
|
4
|
-
import { PluginLoadMessagesFunctionAlreadyDefinedError, PluginSaveMessagesFunctionAlreadyDefinedError, PluginHasInvalidIdError, PluginReturnedInvalidCustomApiError,
|
|
4
|
+
import { PluginLoadMessagesFunctionAlreadyDefinedError, PluginSaveMessagesFunctionAlreadyDefinedError, PluginHasInvalidIdError, PluginReturnedInvalidCustomApiError, PluginsDoNotProvideLoadOrSaveMessagesError, } from "./errors.js";
|
|
5
5
|
it("should return an error if a plugin uses an invalid id", async () => {
|
|
6
6
|
const mockPlugin = {
|
|
7
7
|
// @ts-expect-error - invalid id
|
|
@@ -18,25 +18,6 @@ it("should return an error if a plugin uses an invalid id", async () => {
|
|
|
18
18
|
});
|
|
19
19
|
expect(resolved.errors[0]).toBeInstanceOf(PluginHasInvalidIdError);
|
|
20
20
|
});
|
|
21
|
-
it("should return an error if a plugin uses APIs that are not available", async () => {
|
|
22
|
-
const mockPlugin = {
|
|
23
|
-
id: "plugin.namespace.undefinedApi",
|
|
24
|
-
description: { en: "My plugin description" },
|
|
25
|
-
displayName: { en: "My plugin" },
|
|
26
|
-
// @ts-expect-error the key is not available in type
|
|
27
|
-
nonExistentKey: {
|
|
28
|
-
nonexistentOptions: "value",
|
|
29
|
-
},
|
|
30
|
-
loadMessages: () => undefined,
|
|
31
|
-
saveMessages: () => undefined,
|
|
32
|
-
};
|
|
33
|
-
const resolved = await resolvePlugins({
|
|
34
|
-
plugins: [mockPlugin],
|
|
35
|
-
settings: {},
|
|
36
|
-
nodeishFs: {},
|
|
37
|
-
});
|
|
38
|
-
expect(resolved.errors[0]).toBeInstanceOf(PluginHasInvalidSchemaError);
|
|
39
|
-
});
|
|
40
21
|
it("should expose the project settings including the plugin settings", async () => {
|
|
41
22
|
const settings = {
|
|
42
23
|
sourceLanguageTag: "en",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveModules.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/resolveModules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"resolveModules.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/resolveModules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAoBvD,eAAO,MAAM,cAAc,EAAE,qBAiG5B,CAAA"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { InlangModule } from "@inlang/module";
|
|
2
|
-
import { ModuleError, ModuleImportError, ModuleHasNoExportsError, ModuleExportIsInvalidError, } from "./errors.js";
|
|
2
|
+
import { ModuleError, ModuleImportError, ModuleHasNoExportsError, ModuleExportIsInvalidError, ModuleSettingsAreInvalidError, } from "./errors.js";
|
|
3
3
|
import { tryCatch } from "@inlang/result";
|
|
4
4
|
import { resolveMessageLintRules } from "./message-lint-rules/resolveMessageLintRules.js";
|
|
5
5
|
import { createImport } from "./import.js";
|
|
6
6
|
import { resolvePlugins } from "./plugins/resolvePlugins.js";
|
|
7
7
|
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
8
|
+
import { validatedModuleSettings } from "./validatedModuleSettings.js";
|
|
8
9
|
const ModuleCompiler = TypeCompiler.Compile(InlangModule);
|
|
9
10
|
export const resolveModules = async (args) => {
|
|
10
11
|
const _import = args._import ?? createImport({ readFile: args.nodeishFs.readFile });
|
|
@@ -32,6 +33,7 @@ export const resolveModules = async (args) => {
|
|
|
32
33
|
}));
|
|
33
34
|
continue;
|
|
34
35
|
}
|
|
36
|
+
// -- CHECK IF MODULE IS SYNTACTIALLY VALID
|
|
35
37
|
const isValidModule = ModuleCompiler.Check(importedModule.data);
|
|
36
38
|
if (isValidModule === false) {
|
|
37
39
|
const errors = [...ModuleCompiler.Errors(importedModule.data)];
|
|
@@ -41,6 +43,15 @@ export const resolveModules = async (args) => {
|
|
|
41
43
|
}));
|
|
42
44
|
continue;
|
|
43
45
|
}
|
|
46
|
+
// -- VALIDATE MODULE SETTINGS
|
|
47
|
+
const result = validatedModuleSettings({
|
|
48
|
+
settingsSchema: importedModule.data.default.settingsSchema,
|
|
49
|
+
moduleSettings: args.settings[importedModule.data.default.id],
|
|
50
|
+
});
|
|
51
|
+
if (result !== "isValid") {
|
|
52
|
+
moduleErrors.push(new ModuleSettingsAreInvalidError({ module: module, errors: result }));
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
44
55
|
meta.push({
|
|
45
56
|
module: module,
|
|
46
57
|
id: importedModule.data.default.id,
|
|
@@ -52,7 +63,7 @@ export const resolveModules = async (args) => {
|
|
|
52
63
|
allMessageLintRules.push(importedModule.data.default);
|
|
53
64
|
}
|
|
54
65
|
else {
|
|
55
|
-
|
|
66
|
+
moduleErrors.push(new ModuleError(`Unimplemented module type ${importedModule.data.default.id}.The module has not been installed.`, { module: module }));
|
|
56
67
|
}
|
|
57
68
|
}
|
|
58
69
|
const resolvedPlugins = await resolvePlugins({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { expect, it } from "vitest";
|
|
2
|
-
import { ModuleError, ModuleExportIsInvalidError, ModuleHasNoExportsError, ModuleImportError, } from "./errors.js";
|
|
2
|
+
import { ModuleError, ModuleExportIsInvalidError, ModuleHasNoExportsError, ModuleImportError, ModuleSettingsAreInvalidError, } from "./errors.js";
|
|
3
3
|
import { resolveModules } from "./resolveModules.js";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
4
5
|
it("should return an error if a plugin cannot be imported", async () => {
|
|
5
6
|
const settings = {
|
|
6
7
|
sourceLanguageTag: "en",
|
|
@@ -127,3 +128,27 @@ it("should handle other unhandled errors during plugin resolution", async () =>
|
|
|
127
128
|
// Assert results
|
|
128
129
|
expect(resolved.errors[0]).toBeInstanceOf(ModuleError);
|
|
129
130
|
});
|
|
131
|
+
it("should return an error if a moduleSettings are invalid", async () => {
|
|
132
|
+
const settings = {
|
|
133
|
+
sourceLanguageTag: "en",
|
|
134
|
+
languageTags: ["de", "en"],
|
|
135
|
+
modules: ["plugin.js"],
|
|
136
|
+
"plugin.namespace.mock": {
|
|
137
|
+
ignore: ["invalid"],
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
const _import = async () => ({
|
|
141
|
+
default: {
|
|
142
|
+
id: "plugin.namespace.mock",
|
|
143
|
+
description: { en: "Mock plugin description" },
|
|
144
|
+
displayName: { en: "Mock Plugin" },
|
|
145
|
+
settingsSchema: Type.Object({
|
|
146
|
+
ignore: Type.String(),
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
// Call the function
|
|
151
|
+
const resolved = await resolveModules({ settings, _import, nodeishFs: {} });
|
|
152
|
+
// Assert results
|
|
153
|
+
expect(resolved.errors[0]).toBeInstanceOf(ModuleSettingsAreInvalidError);
|
|
154
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateModuleSettings.test.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/validateModuleSettings.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
import { expect, test } from "vitest";
|
|
3
|
+
import { Plugin, MessageLintRule } from "@inlang/sdk";
|
|
4
|
+
// import { createNodeishMemoryFs } from "@lix-js/fs"
|
|
5
|
+
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import { validatedModuleSettings } from "./validatedModuleSettings.js";
|
|
7
|
+
const mockPluginSchema = Type.Object({
|
|
8
|
+
pathPattern: Type.Union([
|
|
9
|
+
Type.String({
|
|
10
|
+
pattern: "^(\\./|\\../|/)[^*]*\\{languageTag\\}[^*]*\\.json",
|
|
11
|
+
description: "The pathPattern must contain `{languageTag}` and end with `.json`.",
|
|
12
|
+
}),
|
|
13
|
+
Type.Record(Type.String({
|
|
14
|
+
pattern: "^[^.]+$",
|
|
15
|
+
description: "Dots are not allowd ",
|
|
16
|
+
examples: ["website", "app", "homepage"],
|
|
17
|
+
}), Type.String({
|
|
18
|
+
pattern: "^(\\./|\\../|/)[^*]*\\{languageTag\\}[^*]*\\.json",
|
|
19
|
+
description: "The pathPattern must contain `{languageTag}` and end with `.json`.",
|
|
20
|
+
})),
|
|
21
|
+
]),
|
|
22
|
+
variableReferencePattern: Type.Array(Type.String()),
|
|
23
|
+
});
|
|
24
|
+
const mockMessageLintRuleSchema = Type.Object({
|
|
25
|
+
ignore: Type.Array(Type.String({
|
|
26
|
+
pattern: '([^"]*)',
|
|
27
|
+
description: "All items in the array need quotaion marks at the end and beginning",
|
|
28
|
+
})),
|
|
29
|
+
});
|
|
30
|
+
test("if PluginSchema does match with the moduleSettings", async () => {
|
|
31
|
+
const isValid = await validatedModuleSettings({
|
|
32
|
+
settingsSchema: mockPluginSchema,
|
|
33
|
+
moduleSettings: {
|
|
34
|
+
pathPattern: "./examples/example01/{languageTag}.json",
|
|
35
|
+
variableReferencePattern: ["{", "}"],
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
expect(isValid).toBe("isValid");
|
|
39
|
+
});
|
|
40
|
+
test("if namespace settings are valide", async () => {
|
|
41
|
+
const isValid = validatedModuleSettings({
|
|
42
|
+
settingsSchema: mockPluginSchema,
|
|
43
|
+
moduleSettings: {
|
|
44
|
+
pathPattern: {
|
|
45
|
+
website: "./{languageTag}examplerFolder/ExampleFile.json",
|
|
46
|
+
app: "../{languageTag}examplerFolder/ExampleFile.json",
|
|
47
|
+
footer: "./{languageTag}examplerFolder/ExampleFile.json",
|
|
48
|
+
},
|
|
49
|
+
variableReferencePattern: ["{", "}"],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
expect(isValid).toBe("isValid");
|
|
53
|
+
});
|
|
54
|
+
test(" if MessageLintRuleSchema match with the settings", async () => {
|
|
55
|
+
const isValid = await validatedModuleSettings({
|
|
56
|
+
settingsSchema: mockMessageLintRuleSchema,
|
|
57
|
+
moduleSettings: {
|
|
58
|
+
ignore: ["example", "warning"],
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
expect(isValid).toBe("isValid");
|
|
62
|
+
});
|
|
63
|
+
test("if messageLintRule settings are not matching to the settingsSchema would pass ", async () => {
|
|
64
|
+
const isValid = validatedModuleSettings({
|
|
65
|
+
settingsSchema: mockMessageLintRuleSchema,
|
|
66
|
+
moduleSettings: {
|
|
67
|
+
ignore: "example",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
expect(isValid).not.toBe("isValid");
|
|
71
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { InlangModule } from "@inlang/module";
|
|
2
|
+
import { type ValueError } from "@sinclair/typebox/value";
|
|
3
|
+
export declare const validatedModuleSettings: (args: {
|
|
4
|
+
settingsSchema: InlangModule["default"]["settingsSchema"];
|
|
5
|
+
moduleSettings: unknown;
|
|
6
|
+
}) => "isValid" | ValueError[];
|
|
7
|
+
//# sourceMappingURL=validatedModuleSettings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validatedModuleSettings.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/validatedModuleSettings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAEhE,eAAO,MAAM,uBAAuB,SAAU;IAC7C,cAAc,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,gBAAgB,CAAC,CAAA;IACzD,cAAc,EAAE,OAAO,CAAA;CACvB,KAAG,SAAS,GAAG,UAAU,EAWzB,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Value } from "@sinclair/typebox/value";
|
|
2
|
+
export const validatedModuleSettings = (args) => {
|
|
3
|
+
if (args.settingsSchema && args.moduleSettings) {
|
|
4
|
+
const hasValidSettings = Value.Check(args.settingsSchema, args.moduleSettings);
|
|
5
|
+
if (hasValidSettings === false) {
|
|
6
|
+
const errors = [...Value.Errors(args.settingsSchema, args.moduleSettings)];
|
|
7
|
+
return errors;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return "isValid";
|
|
11
|
+
};
|
package/package.json
CHANGED
|
@@ -32,6 +32,7 @@ const mockPlugin: Plugin = {
|
|
|
32
32
|
id: "plugin.project.i18next",
|
|
33
33
|
description: { en: "Mock plugin description" },
|
|
34
34
|
displayName: { en: "Mock Plugin" },
|
|
35
|
+
|
|
35
36
|
loadMessages: () => exampleMessages,
|
|
36
37
|
saveMessages: () => undefined,
|
|
37
38
|
}
|
|
@@ -76,6 +77,7 @@ const mockLintRule: MessageLintRule = {
|
|
|
76
77
|
id: "messageLintRule.namespace.mock",
|
|
77
78
|
description: { en: "Mock lint rule description" },
|
|
78
79
|
displayName: { en: "Mock Lint Rule" },
|
|
80
|
+
|
|
79
81
|
run: () => undefined,
|
|
80
82
|
}
|
|
81
83
|
|
|
@@ -179,6 +181,7 @@ describe("messages", () => {
|
|
|
179
181
|
description: {
|
|
180
182
|
en: "wo",
|
|
181
183
|
},
|
|
184
|
+
|
|
182
185
|
loadMessages: ({ settings }) => (settings.languageTags.length ? exampleMessages : []),
|
|
183
186
|
saveMessages: () => undefined,
|
|
184
187
|
}
|
|
@@ -7,6 +7,7 @@ const lintRule1 = {
|
|
|
7
7
|
id: "messageLintRule.x.1",
|
|
8
8
|
displayName: { en: "" },
|
|
9
9
|
description: { en: "" },
|
|
10
|
+
|
|
10
11
|
run: vi.fn(),
|
|
11
12
|
} satisfies MessageLintRule
|
|
12
13
|
|
|
@@ -14,6 +15,7 @@ const lintRule2 = {
|
|
|
14
15
|
id: "messageLintRule.x.2",
|
|
15
16
|
displayName: { en: "" },
|
|
16
17
|
description: { en: "" },
|
|
18
|
+
|
|
17
19
|
run: vi.fn(),
|
|
18
20
|
} satisfies MessageLintRule
|
|
19
21
|
|
|
@@ -8,6 +8,7 @@ const lintRule1 = {
|
|
|
8
8
|
id: "messageLintRule.r.1",
|
|
9
9
|
displayName: "",
|
|
10
10
|
description: "",
|
|
11
|
+
|
|
11
12
|
run: vi.fn(),
|
|
12
13
|
} satisfies MessageLintRule
|
|
13
14
|
|
|
@@ -15,6 +16,7 @@ const lintRule2 = {
|
|
|
15
16
|
id: "messageLintRule.r.2",
|
|
16
17
|
displayName: "",
|
|
17
18
|
description: "",
|
|
19
|
+
|
|
18
20
|
run: vi.fn(),
|
|
19
21
|
} satisfies MessageLintRule
|
|
20
22
|
|
package/src/loadProject.test.ts
CHANGED
|
@@ -39,6 +39,7 @@ const mockPlugin: Plugin = {
|
|
|
39
39
|
id: "plugin.project.i18next",
|
|
40
40
|
description: { en: "Mock plugin description" },
|
|
41
41
|
displayName: { en: "Mock Plugin" },
|
|
42
|
+
|
|
42
43
|
loadMessages: () => exampleMessages,
|
|
43
44
|
saveMessages: () => undefined as any,
|
|
44
45
|
addCustomApi: () => ({
|
|
@@ -425,6 +426,7 @@ describe("functionality", () => {
|
|
|
425
426
|
id: "plugin.project.i18next",
|
|
426
427
|
description: { en: "Mock plugin description" },
|
|
427
428
|
displayName: { en: "Mock Plugin" },
|
|
429
|
+
|
|
428
430
|
loadMessages: () => [{ id: "some-message", selectors: [], variants: [] }],
|
|
429
431
|
saveMessages: () => undefined,
|
|
430
432
|
}
|
|
@@ -462,6 +464,7 @@ describe("functionality", () => {
|
|
|
462
464
|
id: "messageLintRule.namepsace.mock",
|
|
463
465
|
description: { en: "Mock lint rule description" },
|
|
464
466
|
displayName: { en: "Mock Lint Rule" },
|
|
467
|
+
|
|
465
468
|
run: ({ report }) => {
|
|
466
469
|
report({
|
|
467
470
|
messageId: "some-message",
|
|
@@ -474,6 +477,7 @@ describe("functionality", () => {
|
|
|
474
477
|
id: "plugin.project.i18next",
|
|
475
478
|
description: { en: "Mock plugin description" },
|
|
476
479
|
displayName: { en: "Mock Plugin" },
|
|
480
|
+
|
|
477
481
|
loadMessages: () => [{ id: "some-message", selectors: [], variants: [] }],
|
|
478
482
|
saveMessages: () => undefined,
|
|
479
483
|
}
|
|
@@ -579,6 +583,7 @@ describe("functionality", () => {
|
|
|
579
583
|
id: "plugin.project.json",
|
|
580
584
|
description: { en: "Mock plugin description" },
|
|
581
585
|
displayName: { en: "Mock Plugin" },
|
|
586
|
+
|
|
582
587
|
loadMessages: () => exampleMessages,
|
|
583
588
|
saveMessages: mockSaveFn,
|
|
584
589
|
}
|
|
@@ -753,6 +758,7 @@ describe("functionality", () => {
|
|
|
753
758
|
id: "plugin.placeholder.name",
|
|
754
759
|
description: "Mock plugin description",
|
|
755
760
|
displayName: "Mock Plugin",
|
|
761
|
+
|
|
756
762
|
loadMessages: () => [
|
|
757
763
|
createMessage("first", { en: "first message" }),
|
|
758
764
|
createMessage("second", { en: "second message" }),
|
|
@@ -40,10 +40,30 @@ export class ModuleExportIsInvalidError extends ModuleError {
|
|
|
40
40
|
constructor(options: { module: string; errors: ValueError[] }) {
|
|
41
41
|
super(
|
|
42
42
|
`The export(s) of "${options.module}" are invalid:\n\n${options.errors
|
|
43
|
-
.map(
|
|
43
|
+
.map(
|
|
44
|
+
(error) =>
|
|
45
|
+
`"${error.path}" "${JSON.stringify(error.value, undefined, 2)}": "${error.message}"`
|
|
46
|
+
)
|
|
44
47
|
.join("\n")}`,
|
|
45
48
|
options
|
|
46
49
|
)
|
|
47
50
|
this.name = "ModuleExportIsInvalidError"
|
|
48
51
|
}
|
|
49
52
|
}
|
|
53
|
+
|
|
54
|
+
export class ModuleSettingsAreInvalidError extends ModuleError {
|
|
55
|
+
constructor(options: { module: string; errors: ValueError[] }) {
|
|
56
|
+
super(
|
|
57
|
+
`The settings are invalid of "${options.module}" are invalid:\n\n${options.errors
|
|
58
|
+
.map(
|
|
59
|
+
(error) =>
|
|
60
|
+
`Path "${error.path}" with value "${JSON.stringify(error.value, undefined, 2)}": "${
|
|
61
|
+
error.message
|
|
62
|
+
}"`
|
|
63
|
+
)
|
|
64
|
+
.join("\n")}`,
|
|
65
|
+
options
|
|
66
|
+
)
|
|
67
|
+
this.name = "ModuleSettingsAreInvalidError"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
PluginSaveMessagesFunctionAlreadyDefinedError,
|
|
7
7
|
PluginHasInvalidIdError,
|
|
8
8
|
PluginReturnedInvalidCustomApiError,
|
|
9
|
-
PluginHasInvalidSchemaError,
|
|
10
9
|
PluginsDoNotProvideLoadOrSaveMessagesError,
|
|
11
10
|
} from "./errors.js"
|
|
12
11
|
import type { Plugin } from "@inlang/plugin"
|
|
@@ -31,28 +30,6 @@ it("should return an error if a plugin uses an invalid id", async () => {
|
|
|
31
30
|
expect(resolved.errors[0]).toBeInstanceOf(PluginHasInvalidIdError)
|
|
32
31
|
})
|
|
33
32
|
|
|
34
|
-
it("should return an error if a plugin uses APIs that are not available", async () => {
|
|
35
|
-
const mockPlugin: Plugin = {
|
|
36
|
-
id: "plugin.namespace.undefinedApi",
|
|
37
|
-
description: { en: "My plugin description" },
|
|
38
|
-
displayName: { en: "My plugin" },
|
|
39
|
-
// @ts-expect-error the key is not available in type
|
|
40
|
-
nonExistentKey: {
|
|
41
|
-
nonexistentOptions: "value",
|
|
42
|
-
},
|
|
43
|
-
loadMessages: () => undefined as any,
|
|
44
|
-
saveMessages: () => undefined as any,
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const resolved = await resolvePlugins({
|
|
48
|
-
plugins: [mockPlugin],
|
|
49
|
-
settings: {} as any,
|
|
50
|
-
nodeishFs: {} as any,
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
expect(resolved.errors[0]).toBeInstanceOf(PluginHasInvalidSchemaError)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
33
|
it("should expose the project settings including the plugin settings", async () => {
|
|
57
34
|
const settings: ProjectSettings = {
|
|
58
35
|
sourceLanguageTag: "en",
|
|
@@ -114,12 +91,14 @@ describe("loadMessages", () => {
|
|
|
114
91
|
id: "plugin.namepsace.loadMessagesFirst",
|
|
115
92
|
description: { en: "My plugin description" },
|
|
116
93
|
displayName: { en: "My plugin" },
|
|
94
|
+
|
|
117
95
|
loadMessages: async () => undefined as any,
|
|
118
96
|
}
|
|
119
97
|
const mockPlugin2: Plugin = {
|
|
120
98
|
id: "plugin.namepsace.loadMessagesSecond",
|
|
121
99
|
description: { en: "My plugin description" },
|
|
122
100
|
displayName: { en: "My plugin" },
|
|
101
|
+
|
|
123
102
|
loadMessages: async () => undefined as any,
|
|
124
103
|
}
|
|
125
104
|
|
|
@@ -137,6 +116,7 @@ describe("loadMessages", () => {
|
|
|
137
116
|
id: "plugin.namepsace.loadMessagesFirst",
|
|
138
117
|
description: { en: "My plugin description" },
|
|
139
118
|
displayName: { en: "My plugin" },
|
|
119
|
+
|
|
140
120
|
saveMessages: async () => undefined as any,
|
|
141
121
|
}
|
|
142
122
|
|
|
@@ -181,7 +161,6 @@ describe("saveMessages", () => {
|
|
|
181
161
|
id: "plugin.namepsace.saveMessages2",
|
|
182
162
|
description: { en: "My plugin description" },
|
|
183
163
|
displayName: { en: "My plugin" },
|
|
184
|
-
|
|
185
164
|
saveMessages: async () => undefined as any,
|
|
186
165
|
}
|
|
187
166
|
|
|
@@ -199,6 +178,7 @@ describe("saveMessages", () => {
|
|
|
199
178
|
id: "plugin.namepsace.loadMessagesFirst",
|
|
200
179
|
description: { en: "My plugin description" },
|
|
201
180
|
displayName: { en: "My plugin" },
|
|
181
|
+
|
|
202
182
|
loadMessages: async () => undefined as any,
|
|
203
183
|
}
|
|
204
184
|
|
|
@@ -218,7 +198,6 @@ describe("addCustomApi", () => {
|
|
|
218
198
|
id: "plugin.namespace.placeholder",
|
|
219
199
|
description: { en: "My plugin description" },
|
|
220
200
|
displayName: { en: "My plugin" },
|
|
221
|
-
|
|
222
201
|
addCustomApi: () => ({
|
|
223
202
|
"my-app": {
|
|
224
203
|
messageReferenceMatcher: () => undefined as any,
|
|
@@ -253,7 +232,6 @@ describe("addCustomApi", () => {
|
|
|
253
232
|
id: "plugin.namespace.placeholder2",
|
|
254
233
|
description: { en: "My plugin description" },
|
|
255
234
|
displayName: { en: "My plugin" },
|
|
256
|
-
|
|
257
235
|
addCustomApi: () => ({
|
|
258
236
|
"my-app-3": {
|
|
259
237
|
functionOfMyApp3: () => undefined as any,
|
|
@@ -7,10 +7,12 @@ import {
|
|
|
7
7
|
ModuleExportIsInvalidError,
|
|
8
8
|
ModuleHasNoExportsError,
|
|
9
9
|
ModuleImportError,
|
|
10
|
+
ModuleSettingsAreInvalidError,
|
|
10
11
|
} from "./errors.js"
|
|
11
12
|
import { resolveModules } from "./resolveModules.js"
|
|
12
13
|
import type { ProjectSettings } from "@inlang/project-settings"
|
|
13
14
|
import type { InlangModule } from "@inlang/module"
|
|
15
|
+
import { Type } from "@sinclair/typebox"
|
|
14
16
|
|
|
15
17
|
it("should return an error if a plugin cannot be imported", async () => {
|
|
16
18
|
const settings: ProjectSettings = {
|
|
@@ -39,6 +41,7 @@ it("should resolve plugins and message lint rules successfully", async () => {
|
|
|
39
41
|
id: "plugin.namespace.mock",
|
|
40
42
|
description: { en: "Mock plugin description" },
|
|
41
43
|
displayName: { en: "Mock Plugin" },
|
|
44
|
+
|
|
42
45
|
loadMessages: () => undefined as any,
|
|
43
46
|
saveMessages: () => undefined as any,
|
|
44
47
|
addCustomApi: () => ({
|
|
@@ -52,6 +55,7 @@ it("should resolve plugins and message lint rules successfully", async () => {
|
|
|
52
55
|
id: "messageLintRule.namespace.mock",
|
|
53
56
|
description: { en: "Mock lint rule description" },
|
|
54
57
|
displayName: { en: "Mock Lint Rule" },
|
|
58
|
+
|
|
55
59
|
run: () => undefined,
|
|
56
60
|
}
|
|
57
61
|
|
|
@@ -160,3 +164,30 @@ it("should handle other unhandled errors during plugin resolution", async () =>
|
|
|
160
164
|
// Assert results
|
|
161
165
|
expect(resolved.errors[0]).toBeInstanceOf(ModuleError)
|
|
162
166
|
})
|
|
167
|
+
it("should return an error if a moduleSettings are invalid", async () => {
|
|
168
|
+
const settings: ProjectSettings = {
|
|
169
|
+
sourceLanguageTag: "en",
|
|
170
|
+
languageTags: ["de", "en"],
|
|
171
|
+
modules: ["plugin.js"],
|
|
172
|
+
"plugin.namespace.mock": {
|
|
173
|
+
ignore: ["invalid"],
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const _import = async () => ({
|
|
178
|
+
default: {
|
|
179
|
+
id: "plugin.namespace.mock",
|
|
180
|
+
description: { en: "Mock plugin description" },
|
|
181
|
+
displayName: { en: "Mock Plugin" },
|
|
182
|
+
settingsSchema: Type.Object({
|
|
183
|
+
ignore: Type.String(),
|
|
184
|
+
}),
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// Call the function
|
|
189
|
+
const resolved = await resolveModules({ settings, _import, nodeishFs: {} as any })
|
|
190
|
+
|
|
191
|
+
// Assert results
|
|
192
|
+
expect(resolved.errors[0]).toBeInstanceOf(ModuleSettingsAreInvalidError)
|
|
193
|
+
})
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ModuleImportError,
|
|
6
6
|
ModuleHasNoExportsError,
|
|
7
7
|
ModuleExportIsInvalidError,
|
|
8
|
+
ModuleSettingsAreInvalidError,
|
|
8
9
|
} from "./errors.js"
|
|
9
10
|
import { tryCatch } from "@inlang/result"
|
|
10
11
|
import { resolveMessageLintRules } from "./message-lint-rules/resolveMessageLintRules.js"
|
|
@@ -13,6 +14,7 @@ import { createImport } from "./import.js"
|
|
|
13
14
|
import type { MessageLintRule } from "@inlang/message-lint-rule"
|
|
14
15
|
import { resolvePlugins } from "./plugins/resolvePlugins.js"
|
|
15
16
|
import { TypeCompiler } from "@sinclair/typebox/compiler"
|
|
17
|
+
import { validatedModuleSettings } from "./validatedModuleSettings.js"
|
|
16
18
|
|
|
17
19
|
const ModuleCompiler = TypeCompiler.Compile(InlangModule)
|
|
18
20
|
|
|
@@ -31,8 +33,8 @@ export const resolveModules: ResolveModuleFunction = async (args) => {
|
|
|
31
33
|
*/
|
|
32
34
|
|
|
33
35
|
const importedModule = await tryCatch<InlangModule>(() => _import(module))
|
|
34
|
-
|
|
35
36
|
// -- IMPORT MODULE --
|
|
37
|
+
|
|
36
38
|
if (importedModule.error) {
|
|
37
39
|
moduleErrors.push(
|
|
38
40
|
new ModuleImportError({
|
|
@@ -44,6 +46,7 @@ export const resolveModules: ResolveModuleFunction = async (args) => {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
// -- MODULE DOES NOT EXPORT ANYTHING --
|
|
49
|
+
|
|
47
50
|
if (importedModule.data?.default === undefined) {
|
|
48
51
|
moduleErrors.push(
|
|
49
52
|
new ModuleHasNoExportsError({
|
|
@@ -53,8 +56,9 @@ export const resolveModules: ResolveModuleFunction = async (args) => {
|
|
|
53
56
|
continue
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
// -- CHECK IF MODULE IS SYNTACTIALLY VALID
|
|
57
60
|
|
|
61
|
+
const isValidModule = ModuleCompiler.Check(importedModule.data)
|
|
58
62
|
if (isValidModule === false) {
|
|
59
63
|
const errors = [...ModuleCompiler.Errors(importedModule.data)]
|
|
60
64
|
moduleErrors.push(
|
|
@@ -63,6 +67,18 @@ export const resolveModules: ResolveModuleFunction = async (args) => {
|
|
|
63
67
|
errors,
|
|
64
68
|
})
|
|
65
69
|
)
|
|
70
|
+
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// -- VALIDATE MODULE SETTINGS
|
|
75
|
+
|
|
76
|
+
const result = validatedModuleSettings({
|
|
77
|
+
settingsSchema: importedModule.data.default.settingsSchema,
|
|
78
|
+
moduleSettings: args.settings[importedModule.data.default.id],
|
|
79
|
+
})
|
|
80
|
+
if (result !== "isValid") {
|
|
81
|
+
moduleErrors.push(new ModuleSettingsAreInvalidError({ module: module, errors: result }))
|
|
66
82
|
continue
|
|
67
83
|
}
|
|
68
84
|
|
|
@@ -76,10 +92,14 @@ export const resolveModules: ResolveModuleFunction = async (args) => {
|
|
|
76
92
|
} else if (importedModule.data.default.id.startsWith("messageLintRule.")) {
|
|
77
93
|
allMessageLintRules.push(importedModule.data.default as MessageLintRule)
|
|
78
94
|
} else {
|
|
79
|
-
|
|
95
|
+
moduleErrors.push(
|
|
96
|
+
new ModuleError(
|
|
97
|
+
`Unimplemented module type ${importedModule.data.default.id}.The module has not been installed.`,
|
|
98
|
+
{ module: module }
|
|
99
|
+
)
|
|
100
|
+
)
|
|
80
101
|
}
|
|
81
102
|
}
|
|
82
|
-
|
|
83
103
|
const resolvedPlugins = await resolvePlugins({
|
|
84
104
|
plugins: allPlugins,
|
|
85
105
|
settings: args.settings,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
import { expect, test } from "vitest"
|
|
3
|
+
import { Plugin, MessageLintRule } from "@inlang/sdk"
|
|
4
|
+
// import { createNodeishMemoryFs } from "@lix-js/fs"
|
|
5
|
+
import { Type } from "@sinclair/typebox"
|
|
6
|
+
import { validatedModuleSettings } from "./validatedModuleSettings.js"
|
|
7
|
+
|
|
8
|
+
const mockPluginSchema: Plugin["settingsSchema"] = Type.Object({
|
|
9
|
+
pathPattern: Type.Union([
|
|
10
|
+
Type.String({
|
|
11
|
+
pattern: "^(\\./|\\../|/)[^*]*\\{languageTag\\}[^*]*\\.json",
|
|
12
|
+
description: "The pathPattern must contain `{languageTag}` and end with `.json`.",
|
|
13
|
+
}),
|
|
14
|
+
Type.Record(
|
|
15
|
+
Type.String({
|
|
16
|
+
pattern: "^[^.]+$",
|
|
17
|
+
description: "Dots are not allowd ",
|
|
18
|
+
examples: ["website", "app", "homepage"],
|
|
19
|
+
}),
|
|
20
|
+
Type.String({
|
|
21
|
+
pattern: "^(\\./|\\../|/)[^*]*\\{languageTag\\}[^*]*\\.json",
|
|
22
|
+
description: "The pathPattern must contain `{languageTag}` and end with `.json`.",
|
|
23
|
+
})
|
|
24
|
+
),
|
|
25
|
+
]),
|
|
26
|
+
variableReferencePattern: Type.Array(Type.String()),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const mockMessageLintRuleSchema: MessageLintRule["settingsSchema"] = Type.Object({
|
|
30
|
+
ignore: Type.Array(
|
|
31
|
+
Type.String({
|
|
32
|
+
pattern: '([^"]*)',
|
|
33
|
+
description: "All items in the array need quotaion marks at the end and beginning",
|
|
34
|
+
})
|
|
35
|
+
),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test("if PluginSchema does match with the moduleSettings", async () => {
|
|
39
|
+
const isValid = await validatedModuleSettings({
|
|
40
|
+
settingsSchema: mockPluginSchema,
|
|
41
|
+
moduleSettings: {
|
|
42
|
+
pathPattern: "./examples/example01/{languageTag}.json",
|
|
43
|
+
variableReferencePattern: ["{", "}"],
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(isValid).toBe("isValid")
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("if namespace settings are valide", async () => {
|
|
51
|
+
const isValid = validatedModuleSettings({
|
|
52
|
+
settingsSchema: mockPluginSchema,
|
|
53
|
+
moduleSettings: {
|
|
54
|
+
pathPattern: {
|
|
55
|
+
website: "./{languageTag}examplerFolder/ExampleFile.json",
|
|
56
|
+
app: "../{languageTag}examplerFolder/ExampleFile.json",
|
|
57
|
+
footer: "./{languageTag}examplerFolder/ExampleFile.json",
|
|
58
|
+
},
|
|
59
|
+
variableReferencePattern: ["{", "}"],
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
expect(isValid).toBe("isValid")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test(" if MessageLintRuleSchema match with the settings", async () => {
|
|
66
|
+
const isValid = await validatedModuleSettings({
|
|
67
|
+
settingsSchema: mockMessageLintRuleSchema,
|
|
68
|
+
moduleSettings: {
|
|
69
|
+
ignore: ["example", "warning"],
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
expect(isValid).toBe("isValid")
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test("if messageLintRule settings are not matching to the settingsSchema would pass ", async () => {
|
|
77
|
+
const isValid = validatedModuleSettings({
|
|
78
|
+
settingsSchema: mockMessageLintRuleSchema,
|
|
79
|
+
moduleSettings: {
|
|
80
|
+
ignore: "example",
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
expect(isValid).not.toBe("isValid")
|
|
85
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { InlangModule } from "@inlang/module"
|
|
2
|
+
import type { TSchema } from "@sinclair/typebox"
|
|
3
|
+
import { Value, type ValueError } from "@sinclair/typebox/value"
|
|
4
|
+
|
|
5
|
+
export const validatedModuleSettings = (args: {
|
|
6
|
+
settingsSchema: InlangModule["default"]["settingsSchema"]
|
|
7
|
+
moduleSettings: unknown
|
|
8
|
+
}): "isValid" | ValueError[] => {
|
|
9
|
+
if (args.settingsSchema && args.moduleSettings) {
|
|
10
|
+
const hasValidSettings = Value.Check(args.settingsSchema as TSchema, args.moduleSettings)
|
|
11
|
+
|
|
12
|
+
if (hasValidSettings === false) {
|
|
13
|
+
const errors = [...Value.Errors(args.settingsSchema as TSchema, args.moduleSettings)]
|
|
14
|
+
|
|
15
|
+
return errors
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return "isValid"
|
|
19
|
+
}
|