import { toBlob, type Account, type Lix } from "@lix-js/sdk"; import type { InlangPlugin } from "../plugin/schema.js"; import type { ProjectSettings } from "../json-schema/settings.js"; import { contentFromDatabase, type SqliteWasmDatabase, } from "sqlite-wasm-kysely"; import { initDb } from "../database/initDb.js"; import { importPlugins, type PreprocessPluginBeforeImportFunction, } from "../plugin/importPlugins.js"; import type { InlangProject } from "./api.js"; import { withLanguageTagToLocaleMigration } from "../migrations/v2/withLanguageTagToLocaleMigration.js"; import { v4 } from "uuid"; import { importFiles } from "../import-export/importFiles.js"; import { exportFiles } from "../import-export/exportFiles.js"; /** * Common load project logic. */ export async function loadProject(args: { sqlite: SqliteWasmDatabase; lix: Lix; /** * The account that loaded the project. * * Defaults to an anonymous/new account if undefined. * * @example * const account = localStorage.getItem("account") * const project = await loadProject({ account }) */ account?: Account; /** * Provide plugins to the project. * * This is useful for testing or providing plugins that are * app specific. Keep in mind that provided plugins * are not shared with other instances. */ providePlugins?: InlangPlugin[]; /** * Function that preprocesses the plugin before importing it. * * The callback can be used to process plugins as needed in the * environment of the app. For example, Sherlock uses this to convert * ESM, which all inlang plugins are written in, to CJS which Sherlock * runs in. * * @example * const project = await loadProject({ preprocessPluginBeforeImport: (moduleText) => convertEsmToCjs(moduleText) }) * */ preprocessPluginBeforeImport?: PreprocessPluginBeforeImportFunction; }): Promise { const db = initDb({ sqlite: args.sqlite }); await maybeMigrateFirstProjectId({ lix: args.lix }); const settingsFile = await args.lix.db .selectFrom("file") .select("data") .where("path", "=", "/settings.json") .executeTakeFirstOrThrow(); const settings = withLanguageTagToLocaleMigration( JSON.parse(new TextDecoder().decode(settingsFile.data)) as ProjectSettings ); const importedPlugins = await importPlugins({ settings, lix: args.lix, preprocessPluginBeforeImport: args.preprocessPluginBeforeImport, }); const plugins = [...(args.providePlugins ?? []), ...importedPlugins.plugins]; // const state = createProjectState({ // ...args, // settings, // }); return { db, id: { get: async () => { const file = await args.lix.db .selectFrom("file") .where("path", "=", "/project_id") .select("file.data") .executeTakeFirstOrThrow(); return new TextDecoder().decode(file.data); }, }, settings: { get: async () => { const file = await args.lix.db .selectFrom("file") .where("path", "=", "/settings.json") .select("file.data") .executeTakeFirstOrThrow(); return withLanguageTagToLocaleMigration( JSON.parse(new TextDecoder().decode(file.data)) ); }, set: async (newSettings) => { const cloned = JSON.parse(JSON.stringify(newSettings)); cloned.languageTags = cloned.locales; cloned.sourceLanguageTag = cloned.baseLocale; await args.lix.db .updateTable("file") .where("path", "=", "/settings.json") .set({ data: new TextEncoder().encode( JSON.stringify(cloned, undefined, 2) ), }) .execute(); }, }, plugins: { get: async () => plugins, }, errors: { get: async () => [...importedPlugins.errors], }, // errors: state.errors, importFiles: async ({ files, pluginKey }) => { const settingsFile = await args.lix.db .selectFrom("file") .where("path", "=", "/settings.json") .select("file.data") .executeTakeFirstOrThrow(); const settings = JSON.parse( new TextDecoder().decode(settingsFile.data) ) as ProjectSettings; return await importFiles({ files, pluginKey, settings, // TODO don't use global state, might be stale plugins, db, }); }, exportFiles: async ({ pluginKey }) => { const settingsFile = await args.lix.db .selectFrom("file") .where("path", "=", "/settings.json") .select("file.data") .executeTakeFirstOrThrow(); const settings = JSON.parse( new TextDecoder().decode(settingsFile.data) ) as ProjectSettings; return ( await exportFiles({ pluginKey, db, settings, // TODO don't use global state, might be stale plugins, }) ).map((output) => ({ ...output, pluginKey })); }, close: async () => { await saveDbToLix({ sqlite: args.sqlite, lix: args.lix }); await db.destroy(); await args.lix.db.destroy(); }, _sqlite: args.sqlite, toBlob: async () => { await saveDbToLix({ sqlite: args.sqlite, lix: args.lix }); return await toBlob({ lix: args.lix }); }, lix: args.lix, }; } async function saveDbToLix(args: { sqlite: SqliteWasmDatabase; lix: Lix; }): Promise { const data = contentFromDatabase(args.sqlite); await args.lix.db .updateTable("file") .set("data", data) .where("path", "=", "/db.sqlite") .execute(); } /** * Old leftover migration from v1. Probably not needed anymore. * * Kept it in just in case. */ async function maybeMigrateFirstProjectId(args: { lix: Lix }): Promise { const firstProjectIdFile = await args.lix.db .selectFrom("file") .select("data") .where("path", "=", "/project_id") .executeTakeFirst(); if (!firstProjectIdFile) { await args.lix.db .insertInto("file") .values({ path: "/project_id", data: new TextEncoder().encode(v4()), }) .execute(); } }