Skip to content

Commit

Permalink
Merge pull request #3336 from opral/mesdk-244-error-opening-inlang-pr…
Browse files Browse the repository at this point in the history
…oject-at-projectinlang-error-plugins

Mesdk-244-error-opening-inlang-project-at-projectinlang-error-plugins
  • Loading branch information
samuelstroschein authored Jan 10, 2025
2 parents d09e222 + 9e6205d commit f7319b7
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 131 deletions.
2 changes: 1 addition & 1 deletion inlang/packages/paraglide-js/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@inlang/paraglide-js",
"type": "module",
"version": "2.0.0-beta.4",
"version": "2.0.0-beta.5",
"license": "MIT",
"publishConfig": {
"access": "public",
Expand Down
48 changes: 15 additions & 33 deletions inlang/packages/rpc/src/functions/machineTranslateBundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,9 @@ test.runIf(ENV_VARIABLES.GOOGLE_TRANSLATE_API_KEY)(
messageId: "mock-message-id",
matches: [
{
type: "match",
name: "name",
value: {
type: "literal",
value: "John",
},
type: "literal-match",
key: "name",
value: "John",
},
],
pattern: [{ type: "text", value: "Hello world, John" }],
Expand All @@ -114,11 +111,8 @@ test.runIf(ENV_VARIABLES.GOOGLE_TRANSLATE_API_KEY)(
messageId: "mock-message-id",
matches: [
{
type: "match",
name: "name",
value: {
type: "catch-all",
},
type: "catchall-match",
key: "name",
},
],
pattern: [{ type: "text", value: "Hello world" }],
Expand All @@ -127,24 +121,18 @@ test.runIf(ENV_VARIABLES.GOOGLE_TRANSLATE_API_KEY)(
expect.objectContaining({
matches: [
{
type: "match",
name: "name",
value: {
type: "literal",
value: "John",
},
type: "literal-match",
key: "name",
value: "John",
},
],
pattern: [{ type: "text", value: "Hallo Welt, John" }],
}),
expect.objectContaining({
matches: [
{
type: "match",
name: "name",
value: {
type: "catch-all",
},
type: "catchall-match",
key: "name",
},
],
pattern: [{ type: "text", value: "Hallo Welt" }],
Expand All @@ -153,24 +141,18 @@ test.runIf(ENV_VARIABLES.GOOGLE_TRANSLATE_API_KEY)(
expect.objectContaining({
matches: [
{
type: "match",
name: "name",
value: {
type: "literal",
value: "John",
},
type: "literal-match",
key: "name",
value: "John",
},
],
pattern: [{ type: "text", value: "Bonjour tout le monde, John" }],
}),
expect.objectContaining({
matches: [
{
type: "match",
name: "name",
value: {
type: "catch-all",
},
type: "catchall-match",
key: "name",
},
],
pattern: [{ type: "text", value: "Bonjour le monde" }],
Expand Down
2 changes: 1 addition & 1 deletion inlang/packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@inlang/sdk",
"version": "2.0.0-beta.5",
"version": "2.0.0-beta.6",
"type": "module",
"license": "MIT",
"publishConfig": {
Expand Down
1 change: 0 additions & 1 deletion inlang/packages/sdk/src/plugin/importPlugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ test("it should preprocess a plugin", async () => {
},
});

expect(global.fetch).toHaveBeenCalledTimes(1);
expect(result.plugins.length).toBe(1);
expect(result.errors.length).toBe(0);
expect(result.plugins[0]?.key).toBe("preprocessed");
Expand Down
70 changes: 69 additions & 1 deletion inlang/packages/sdk/src/project/loadProjectFromDirectory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,6 @@ test("it should pass toBeImportedMetadata on import", async () => {
expect.objectContaining({
files: [
expect.objectContaining({
name: "en.json",
toBeImportedFilesMetadata: {
foo: "bar",
},
Expand Down Expand Up @@ -852,3 +851,72 @@ test("it can import plugins via http", async () => {
"expecting the plugin to be cached"
).toBe(true);
});

test("plugins that provide both loadMessages and importFiles should be allowed and the importFiles should be called", async () => {
const mockRepo = {
"/project.inlang/settings.json": JSON.stringify({
baseLocale: "en",
locales: ["en"],
modules: [],
} satisfies ProjectSettings),
};

const fs = Volume.fromJSON(mockRepo);

const mockPlugin: InlangPlugin = {
key: "mock-plugin",
loadMessages: vi.fn(async () => []),
importFiles: vi.fn(async () => {
return { bundles: [], messages: [], variants: [] };
}),
};

const project = await loadProjectFromDirectory({
fs: fs as any,
path: "/project.inlang",
providePlugins: [mockPlugin],
});

expect(mockPlugin.importFiles).toHaveBeenCalled();
expect(mockPlugin.loadMessages).not.toHaveBeenCalled();
});

test("providing multiple plugins that have legacy loadMessages and saveMessages function should be possible if they have import/export functions", async () => {
const mockRepo = {
"/project.inlang/settings.json": JSON.stringify({
baseLocale: "en",
locales: ["en"],
modules: [],
} satisfies ProjectSettings),
};

const fs = Volume.fromJSON(mockRepo);

const mockPlugin1: InlangPlugin = {
key: "mock-plugin1",
loadMessages: vi.fn(async () => []),
importFiles: vi.fn(async () => {
return { bundles: [], messages: [], variants: [] };
}),
};

const mockPlugin2: InlangPlugin = {
key: "mock-plugin2",
loadMessages: vi.fn(async () => []),
importFiles: vi.fn(async () => {
return { bundles: [], messages: [], variants: [] };
}),
};

const project = await loadProjectFromDirectory({
fs: fs as any,
path: "/project.inlang",
providePlugins: [mockPlugin1, mockPlugin2],
});

expect(mockPlugin1.importFiles).toHaveBeenCalled();
expect(mockPlugin1.loadMessages).not.toHaveBeenCalled();

expect(mockPlugin2.importFiles).toHaveBeenCalled();
expect(mockPlugin2.loadMessages).not.toHaveBeenCalled();
});
134 changes: 40 additions & 94 deletions inlang/packages/sdk/src/project/loadProjectFromDirectory.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { newProject } from "./newProject.js";
import { loadProjectInMemory } from "./loadProjectInMemory.js";
import { type Lix } from "@lix-js/sdk";

import fs from "node:fs";

import nodePath from "node:path";
import type {
InlangPlugin,
Expand All @@ -14,6 +12,7 @@ import type { ProjectSettings } from "../json-schema/settings.js";
import type { PreprocessPluginBeforeImportFunction } from "../plugin/importPlugins.js";
import { PluginImportError } from "../plugin/errors.js";
import { upsertBundleNestedMatchByProperties } from "../import-export/upsertBundleNestedMatchByProperties.js";
import type { ImportFile } from "./api.js";

/**
* Loads a project from a directory.
Expand Down Expand Up @@ -62,69 +61,39 @@ export async function loadProjectFromDirectory(
syncInterval: args.syncInterval,
});

const {
loadMessagesPlugins,
saveMessagesPlugins,
importPlugins,
exportPlugins,
} = categorizePlugins(await project.plugins.get());
const allPlugins = await project.plugins.get();
const { loadSavePlugins, importExportPlugins } =
categorizePlugins(allPlugins);

// TODO i guess we should move this validation logic into sdk2/src/project/loadProject.ts
// Two scenarios could arise:
// 1. set settings is called from an app - it should detect and reject the setting of settings -> app need to be able to validate before calling set
// 2. the settings file loaded from disc here is corrupted -> user has to fix the file on disc
if (loadMessagesPlugins.length > 1 || saveMessagesPlugins.length > 1) {
if (loadSavePlugins.length > 1) {
throw new Error(
"Max one loadMessages (found: " +
loadMessagesPlugins.length +
loadSavePlugins.length +
") and one saveMessages plugins (found: " +
saveMessagesPlugins.length +
loadSavePlugins.length +
") are allowed "
);
}
const importedResourceFileErrors: Error[] = [];

if (
(loadMessagesPlugins.length > 0 || saveMessagesPlugins.length > 0) &&
(exportPlugins.length > 0 || importPlugins.length > 0)
) {
throw new Error(
"Plugins for loadMessages (found: " +
loadMessagesPlugins.length +
") and saveMessages plugins (found: " +
saveMessagesPlugins.length +
") must not coexist with import (found: " +
importPlugins.length +
") or export (found: " +
exportPlugins.length +
") "
);
} else if (loadMessagesPlugins.length > 1 || saveMessagesPlugins.length > 1) {
throw new Error(
"Max one loadMessages (found: " +
loadMessagesPlugins.length +
") and one saveMessages plugins (found: " +
saveMessagesPlugins.length +
") are allowed "
);
} else if (importPlugins[0]) {
const importer = importPlugins[0];
const files = [];

if (importer.toBeImportedFiles) {
const toBeImportedFiles = await importer.toBeImportedFiles({
// import files from local fs
for (const plugin of importExportPlugins) {
const files: ImportFile[] = [];
if (plugin.toBeImportedFiles) {
const toBeImportedFiles = await plugin.toBeImportedFiles({
settings: await project.settings.get(),
});
for (const toBeImported of toBeImportedFiles) {
const absolute = absolutePathFromProject(args.path, toBeImported.path);
try {
const data = await args.fs.promises.readFile(absolute);
const name = nodePath.basename(toBeImported.path);
files.push({
name,
locale: toBeImported.locale,
content: data,
pluginKey: importer.key,
toBeImportedFilesMetadata: toBeImported.metadata,
});
} catch (e) {
Expand All @@ -143,17 +112,18 @@ export async function loadProjectFromDirectory(
}

await project.importFiles({
pluginKey: importer.key,
files: files as any,
pluginKey: plugin.key,
files,
});
} else if (loadMessagesPlugins[0] !== undefined) {
// TODO create resource files from loadMessageFn call - to poll?
}

for (const plugin of loadSavePlugins) {
await loadLegacyMessages({
project,
pluginKey: plugin.key ?? plugin.id,
loadMessagesFn: plugin.loadMessages!,
projectPath: args.path,
fs: args.fs,
pluginKey: loadMessagesPlugins[0].key ?? loadMessagesPlugins[0].id,
loadMessagesFn: loadMessagesPlugins[0].loadMessages,
});
}

Expand Down Expand Up @@ -583,52 +553,28 @@ async function upsertFileInLix(
)
.execute();
}
/**
* Filters legacy load and save messages plugins.
*
* Legacy plugins are plugins that implement loadMessages and saveMessages but not importFiles and exportFiles.
*/
function categorizePlugins(plugins: readonly InlangPlugin[]) {
const loadSavePlugins: InlangPlugin[] = [];
const importExportPlugins: InlangPlugin[] = [];

for (const plugin of plugins) {
if (
plugin.loadMessages &&
plugin.saveMessages &&
!(plugin.importFiles && plugin.exportFiles)
) {
loadSavePlugins.push(plugin);
} else if (plugin.importFiles || plugin.exportFiles) {
importExportPlugins.push(plugin);
}
}

// TODO i guess we should move this validation logic into sdk2/src/project/loadProject.ts
function categorizePlugins(plugins: readonly InlangPlugin[]): {
loadMessagesPlugins: (InlangPlugin &
Required<Pick<InlangPlugin, "loadMessages">>)[];
saveMessagesPlugins: (InlangPlugin &
Required<Pick<InlangPlugin, "saveMessages">>)[];
importPlugins: (InlangPlugin &
Required<Pick<InlangPlugin, "importFiles" | "toBeImportedFiles">>)[];
exportPlugins: (InlangPlugin & Required<Pick<InlangPlugin, "exportFiles">>)[];
} {
const loadMessagesPlugins = plugins.filter(
(
plugin
): plugin is InlangPlugin & Required<Pick<InlangPlugin, "loadMessages">> =>
plugin.loadMessages !== undefined
);

const saveMessagesPlugins = plugins.filter(
(
plugin
): plugin is InlangPlugin & Required<Pick<InlangPlugin, "saveMessages">> =>
plugin.saveMessages !== undefined
);

const importPlugins = plugins.filter(
(
plugin
): plugin is InlangPlugin &
Required<Pick<InlangPlugin, "importFiles" | "toBeImportedFiles">> =>
plugin.importFiles !== undefined && plugin.toBeImportedFiles !== undefined
);

const exportPlugins = plugins.filter(
(
plugin
): plugin is InlangPlugin & Required<Pick<InlangPlugin, "exportFiles">> =>
plugin.exportFiles !== undefined
);

return {
loadMessagesPlugins,
saveMessagesPlugins,
importPlugins,
exportPlugins,
};
return { loadSavePlugins, importExportPlugins };
}

/**
Expand Down

0 comments on commit f7319b7

Please sign in to comment.