1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-08 13:06:15 +02:00

Chore: Refactor InteropService to not use dynamic imports (#8454)

This commit is contained in:
Henry Heino 2023-07-12 02:30:38 -07:00 committed by GitHub
parent 93e4004033
commit d95d6733a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 341 additions and 244 deletions

View File

@ -638,6 +638,8 @@ packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js
packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/InteropService_Importer_Raw.test.js
packages/lib/services/interop/Module.js
packages/lib/services/interop/Module.test.js
packages/lib/services/interop/types.js
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
packages/lib/services/keychain/KeychainService.js

2
.gitignore vendored
View File

@ -623,6 +623,8 @@ packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js
packages/lib/services/interop/InteropService_Importer_Raw.js
packages/lib/services/interop/InteropService_Importer_Raw.test.js
packages/lib/services/interop/Module.js
packages/lib/services/interop/Module.test.js
packages/lib/services/interop/types.js
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
packages/lib/services/keychain/KeychainService.js

View File

@ -1,7 +1,8 @@
import InteropService from '@joplin/lib/services/interop/InteropService';
import CommandService from '@joplin/lib/services/CommandService';
import shim from '@joplin/lib/shim';
import { ExportOptions, FileSystemItem, Module } from '@joplin/lib/services/interop/types';
import { ExportOptions, FileSystemItem } from '@joplin/lib/services/interop/types';
import { ExportModule } from '@joplin/lib/services/interop/Module';
import { _ } from '@joplin/lib/locale';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
@ -152,7 +153,7 @@ export default class InteropServiceHelper {
}
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
public static async export(_dispatch: Function, module: Module, options: ExportNoteOptions = null) {
public static async export(_dispatch: Function, module: ExportModule, options: ExportNoteOptions = null) {
if (!options) options = {};
let path = null;

View File

@ -9,7 +9,7 @@ import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins
import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
import versionInfo from '@joplin/lib/versionInfo';
import { Module } from '@joplin/lib/services/interop/types';
import { ImportModule } from '@joplin/lib/services/interop/Module';
import InteropServiceHelper from '../InteropServiceHelper';
import { _ } from '@joplin/lib/locale';
import { isContextMenuItemLocation, MenuItem, MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
@ -230,7 +230,7 @@ function useMenu(props: Props) {
void CommandService.instance().execute(commandName);
}, []);
const onImportModuleClick = useCallback(async (module: Module, moduleSource: string) => {
const onImportModuleClick = useCallback(async (module: ImportModule, moduleSource: string) => {
let path = null;
if (moduleSource === 'file') {

View File

@ -1,5 +1,5 @@
import InteropService from '../../services/interop/InteropService';
import { CustomExportContext, CustomImportContext, Module, ModuleType } from '../../services/interop/types';
import { CustomExportContext, CustomImportContext, ModuleType } from '../../services/interop/types';
import shim from '../../shim';
import { fileContentEqual, setupDatabaseAndSynchronizer, switchClient, checkThrowAsync, exportDir, supportDir } from '../../testing/test-utils';
import Folder from '../../models/Folder';
@ -10,6 +10,9 @@ import * as fs from 'fs-extra';
import { FolderEntity, NoteEntity, ResourceEntity } from '../database/types';
import { ModelType } from '../../BaseModel';
import * as ArrayUtils from '../../ArrayUtils';
import InteropService_Importer_Custom from './InteropService_Importer_Custom';
import InteropService_Exporter_Custom from './InteropService_Exporter_Custom';
import Module, { makeExportModule, makeImportModule } from './Module';
async function recreateExportDir() {
const dir = exportDir();
@ -47,35 +50,35 @@ function memoryExportModule() {
resources: [],
};
const module: Module = {
type: ModuleType.Exporter,
const module: Module = makeExportModule({
description: 'Memory Export Module',
format: 'memory',
fileExtensions: ['memory'],
isCustom: true,
}, () => {
return new InteropService_Exporter_Custom({
onInit: async (context: CustomExportContext) => {
result.destPath = context.destPath;
},
onInit: async (context: CustomExportContext) => {
result.destPath = context.destPath;
},
onProcessItem: async (_context: CustomExportContext, itemType: number, item: any) => {
result.items.push({
type: itemType,
object: item,
});
},
onProcessItem: async (_context: CustomExportContext, itemType: number, item: any) => {
result.items.push({
type: itemType,
object: item,
});
},
onProcessResource: async (_context: CustomExportContext, resource: any, filePath: string) => {
result.resources.push({
filePath: filePath,
object: resource,
});
},
onProcessResource: async (_context: CustomExportContext, resource: any, filePath: string) => {
result.resources.push({
filePath: filePath,
object: resource,
});
},
onClose: async (_context: CustomExportContext) => {
// nothing
},
};
onClose: async (_context: CustomExportContext) => {
// nothing
},
});
});
return { result, module };
}
@ -555,18 +558,19 @@ describe('services_InteropService', () => {
sourcePath: '',
};
const module: Module = {
const module = makeImportModule({
type: ModuleType.Importer,
description: 'Test Import Module',
format: 'testing',
fileExtensions: ['test'],
isCustom: true,
onExec: async (context: CustomImportContext) => {
result.hasBeenExecuted = true;
result.sourcePath = context.sourcePath;
},
};
}, () => {
return new InteropService_Importer_Custom({
onExec: async (context: CustomImportContext) => {
result.hasBeenExecuted = true;
result.sourcePath = context.sourcePath;
},
});
});
const service = InteropService.instance();
service.registerModule(module);
@ -596,31 +600,32 @@ describe('services_InteropService', () => {
closeCalled: false,
};
const module: Module = {
const module: Module = makeExportModule({
type: ModuleType.Exporter,
description: 'Test Export Module',
format: 'testing',
fileExtensions: ['test'],
isCustom: true,
}, () => {
return new InteropService_Exporter_Custom({
onInit: async (context: CustomExportContext) => {
result.destPath = context.destPath;
},
onInit: async (context: CustomExportContext) => {
result.destPath = context.destPath;
},
onProcessItem: async (_context: CustomExportContext, itemType: number, item: any) => {
result.itemTypes.push(itemType);
result.items.push(item);
},
onProcessItem: async (_context: CustomExportContext, itemType: number, item: any) => {
result.itemTypes.push(itemType);
result.items.push(item);
},
onProcessResource: async (_context: CustomExportContext, resource: any, filePath: string) => {
result.resources.push(resource);
result.filePaths.push(filePath);
},
onProcessResource: async (_context: CustomExportContext, resource: any, filePath: string) => {
result.resources.push(resource);
result.filePaths.push(filePath);
},
onClose: async (_context: CustomExportContext) => {
result.closeCalled = true;
},
};
onClose: async (_context: CustomExportContext) => {
result.closeCalled = true;
},
});
});
const service = InteropService.instance();
service.registerModule(module);

View File

@ -1,6 +1,4 @@
import { ModuleType, FileSystemItem, ImportModuleOutputFormat, Module, ImportOptions, ExportOptions, ImportExportResult, defaultImportExportModule } from './types';
import InteropService_Importer_Custom from './InteropService_Importer_Custom';
import InteropService_Exporter_Custom from './InteropService_Exporter_Custom';
import { ModuleType, FileSystemItem, ImportModuleOutputFormat, ImportOptions, ExportOptions, ImportExportResult } from './types';
import shim from '../../shim';
import { _ } from '../../locale';
import BaseItem from '../../models/BaseItem';
@ -10,9 +8,22 @@ import Folder from '../../models/Folder';
import NoteTag from '../../models/NoteTag';
import Note from '../../models/Note';
import * as ArrayUtils from '../../ArrayUtils';
import InteropService_Importer_Jex from './InteropService_Importer_Jex';
import InteropService_Importer_Md from './InteropService_Importer_Md';
import InteropService_Importer_Md_frontmatter from './InteropService_Importer_Md_frontmatter';
import InteropService_Importer_Raw from './InteropService_Importer_Raw';
import InteropService_Importer_EnexToMd from './InteropService_Importer_EnexToMd';
import InteropService_Importer_EnexToHtml from './InteropService_Importer_EnexToHtml';
import InteropService_Exporter_Jex from './InteropService_Exporter_Jex';
import InteropService_Exporter_Raw from './InteropService_Exporter_Raw';
import InteropService_Exporter_Md from './InteropService_Exporter_Md';
import InteropService_Exporter_Md_frontmatter from './InteropService_Exporter_Md_frontmatter';
import InteropService_Exporter_Html from './InteropService_Exporter_Html';
import InteropService_Importer_Base from './InteropService_Importer_Base';
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
import Module, { makeExportModule, makeImportModule } from './Module';
const { sprintf } = require('sprintf-js');
const { fileExtension } = require('../../path-utils');
const { toTitleCase } = require('../../string-utils');
const EventEmitter = require('events');
export default class InteropService {
@ -43,47 +54,46 @@ export default class InteropService {
public modules() {
if (!this.defaultModules_) {
const importModules: Module[] = [
{
...defaultImportExportModule(ModuleType.Importer),
const importModules = [
makeImportModule({
format: 'jex',
fileExtensions: ['jex'],
sources: [FileSystemItem.File],
description: _('Joplin Export File'),
},
{
...defaultImportExportModule(ModuleType.Importer),
}, () => new InteropService_Importer_Jex()),
makeImportModule({
format: 'md',
fileExtensions: ['md', 'markdown', 'txt', 'html'],
sources: [FileSystemItem.File, FileSystemItem.Directory],
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
description: _('Markdown'),
},
{
...defaultImportExportModule(ModuleType.Importer),
}, () => new InteropService_Importer_Md()),
makeImportModule({
format: 'md_frontmatter',
fileExtensions: ['md', 'markdown', 'txt', 'html'],
sources: [FileSystemItem.File, FileSystemItem.Directory],
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
description: _('Markdown + Front Matter'),
},
{
...defaultImportExportModule(ModuleType.Importer),
}, () => new InteropService_Importer_Md_frontmatter()),
makeImportModule({
format: 'raw',
sources: [FileSystemItem.Directory],
description: _('Joplin Export Directory'),
},
{
...defaultImportExportModule(ModuleType.Importer),
}, () => new InteropService_Importer_Raw()),
makeImportModule({
format: 'enex',
fileExtensions: ['enex'],
sources: [FileSystemItem.File],
description: _('Evernote Export File (as Markdown)'),
importerClass: 'InteropService_Importer_EnexToMd',
isDefault: true,
},
{
...defaultImportExportModule(ModuleType.Importer),
}, () => new InteropService_Importer_EnexToMd()),
makeImportModule({
format: 'enex',
fileExtensions: ['enex'],
sources: [FileSystemItem.File],
@ -91,65 +101,58 @@ export default class InteropService {
// TODO: Consider doing this the same way as the multiple `md` importers are handled
importerClass: 'InteropService_Importer_EnexToHtml',
outputFormat: ImportModuleOutputFormat.Html,
},
}, () => new InteropService_Importer_EnexToHtml()),
];
const exportModules: Module[] = [
{
...defaultImportExportModule(ModuleType.Exporter),
const exportModules = [
makeExportModule({
format: 'jex',
fileExtensions: ['jex'],
target: FileSystemItem.File,
description: _('Joplin Export File'),
},
{
...defaultImportExportModule(ModuleType.Exporter),
}, () => new InteropService_Exporter_Jex()),
makeExportModule({
format: 'raw',
target: FileSystemItem.Directory,
description: _('Joplin Export Directory'),
},
{
...defaultImportExportModule(ModuleType.Exporter),
}, () => new InteropService_Exporter_Raw()),
makeExportModule({
format: 'md',
target: FileSystemItem.Directory,
description: _('Markdown'),
},
{
...defaultImportExportModule(ModuleType.Exporter),
}, () => new InteropService_Exporter_Md()),
makeExportModule({
format: 'md_frontmatter',
target: FileSystemItem.Directory,
description: _('Markdown + Front Matter'),
},
{
...defaultImportExportModule(ModuleType.Exporter),
}, () => new InteropService_Exporter_Md_frontmatter()),
makeExportModule({
format: 'html',
fileExtensions: ['html', 'htm'],
target: FileSystemItem.File,
isNoteArchive: false,
description: _('HTML File'),
},
{
...defaultImportExportModule(ModuleType.Exporter),
}, () => new InteropService_Exporter_Html()),
makeExportModule({
format: 'html',
target: FileSystemItem.Directory,
description: _('HTML Directory'),
},
}, () => new InteropService_Exporter_Html()),
];
this.defaultModules_ = importModules.concat(exportModules);
this.defaultModules_ = (importModules as Module[]).concat(exportModules);
}
return this.defaultModules_.concat(this.userModules_);
}
public registerModule(module: Module) {
module = {
...defaultImportExportModule(module.type),
...module,
};
this.userModules_.push(module);
this.eventEmitter_.emit('modulesChanged');
}
@ -166,9 +169,13 @@ export default class InteropService {
if (m.format === format && m.type === type) {
if (!target && !outputFormat) {
matches.push(m);
} else if (target && target === m.target) {
} else if (
m.type === ModuleType.Exporter && target && target === m.target
) {
matches.push(m);
} else if (outputFormat && outputFormat === m.outputFormat) {
} else if (
m.type === ModuleType.Importer && outputFormat && outputFormat === m.outputFormat
) {
matches.push(m);
}
}
@ -180,24 +187,6 @@ export default class InteropService {
return matches.length ? matches[0] : null;
}
private modulePath(module: Module) {
let className = '';
if (module.type === ModuleType.Importer) {
className = module.importerClass || `InteropService_Importer_${toTitleCase(module.format)}`;
} else {
className = `InteropService_Exporter_${toTitleCase(module.format)}`;
}
return `./${className}`;
}
private newModuleFromCustomFactory(module: Module) {
if (module.type === ModuleType.Importer) {
return new InteropService_Importer_Custom(module);
} else {
return new InteropService_Exporter_Custom(module);
}
}
// NOTE TO FUTURE SELF: It might make sense to simply move all the existing
// formatters to the `newModuleFromPath_` approach, so that there's only one way
// to do this mapping. This isn't a priority right now (per the convo in:
@ -207,18 +196,7 @@ export default class InteropService {
const moduleMetadata = this.findModuleByFormat_(type, format, null, outputFormat);
if (!moduleMetadata) throw new Error(_('Cannot load "%s" module for format "%s" and output "%s"', type, format, outputFormat));
let output = null;
if (moduleMetadata.isCustom) {
output = this.newModuleFromCustomFactory(moduleMetadata);
} else {
const ModuleClass = shim.requireDynamic(this.modulePath(moduleMetadata)).default;
output = new ModuleClass();
}
output.setMetadata(moduleMetadata);
return output;
return moduleMetadata.factory();
}
// The existing `newModuleByFormat_` fn would load by the input format. This
@ -231,19 +209,7 @@ export default class InteropService {
const moduleMetadata = this.findModuleByFormat_(type, options.format, options.target);
if (!moduleMetadata) throw new Error(_('Cannot load "%s" module for format "%s" and target "%s"', type, options.format, options.target));
let output = null;
if (moduleMetadata.isCustom) {
output = this.newModuleFromCustomFactory(moduleMetadata);
} else {
const modulePath = this.modulePath(moduleMetadata);
const ModuleClass = shim.requireDynamic(modulePath).default;
output = new ModuleClass();
}
output.setMetadata({ options, ...moduleMetadata });
return output;
return moduleMetadata.factory(options);
}
private moduleByFileExtension_(type: ModuleType, ext: string) {
@ -254,7 +220,7 @@ export default class InteropService {
for (let i = 0; i < modules.length; i++) {
const m = modules[i];
if (type !== m.type) continue;
if (m.fileExtensions && m.fileExtensions.indexOf(ext) >= 0) return m;
if (m.fileExtensions.includes(ext)) return m;
}
return null;
@ -273,14 +239,12 @@ export default class InteropService {
if (options.format === 'auto') {
const module = this.moduleByFileExtension_(ModuleType.Importer, fileExtension(options.path));
if (!module) throw new Error(_('Please specify import format for %s', options.path));
// eslint-disable-next-line require-atomic-updates
options.format = module.format;
}
if (options.destinationFolderId) {
const folder = await Folder.load(options.destinationFolderId);
if (!folder) throw new Error(_('Cannot find "%s".', options.destinationFolderId));
// eslint-disable-next-line require-atomic-updates
options.destinationFolder = folder;
}
@ -288,6 +252,10 @@ export default class InteropService {
const importer = this.newModuleByFormat_(ModuleType.Importer, options.format, options.outputFormat);
if (!(importer instanceof InteropService_Importer_Base)) {
throw new Error('Resolved importer is not an importer');
}
await importer.init(options.path, options);
result = await importer.exec(result);
@ -386,6 +354,10 @@ export default class InteropService {
}
const exporter = this.newModuleFromPath_(ModuleType.Exporter, options);
if (!(exporter instanceof InteropService_Exporter_Base)) {
throw new Error('Resolved exporter is not an exporter');
}
await exporter.init(exportPath, options);
const typeOrder = [BaseModel.TYPE_FOLDER, BaseModel.TYPE_RESOURCE, BaseModel.TYPE_NOTE, BaseModel.TYPE_TAG, BaseModel.TYPE_NOTE_TAG];

View File

@ -1,13 +1,20 @@
import { ExportContext } from '../plugins/api/types';
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
import { ExportOptions, Module } from './types';
import { ExportOptions } from './types';
interface CustomImporter {
onInit(context: any): Promise<void>;
onProcessItem(context: any, itemType: number, item: any): Promise<void>;
onProcessResource(context: any, resource: any, filePath: string): Promise<void>;
onClose(context: any): Promise<void>;
}
export default class InteropService_Exporter_Custom extends InteropService_Exporter_Base {
private customContext_: ExportContext;
private module_: Module = null;
private module_: CustomImporter = null;
public constructor(module: Module) {
public constructor(module: CustomImporter) {
super();
this.module_ = module;
}

View File

@ -1,11 +1,17 @@
import InteropService_Importer_Base from './InteropService_Importer_Base';
import { ImportExportResult, Module } from './types';
import { ImportExportResult } from './types';
interface CustomImporter {
onExec(
context: { sourcePath: string; options: any; warnings: string[] }
): Promise<void>;
}
export default class InteropService_Importer_Custom extends InteropService_Importer_Base {
private module_: Module = null;
private module_: CustomImporter = null;
public constructor(handler: Module) {
public constructor(handler: CustomImporter) {
super();
this.module_ = handler;
}
@ -23,10 +29,12 @@ export default class InteropService_Importer_Custom extends InteropService_Impor
}
}
return this.module_.onExec({
await this.module_.onExec({
sourcePath: this.sourcePath_,
options: processedOptions,
warnings: result.warnings,
});
return result;
}
}

View File

@ -0,0 +1,53 @@
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
import InteropService_Importer_Base from './InteropService_Importer_Base';
import { makeExportModule, makeImportModule } from './Module';
import { FileSystemItem } from './types';
describe('Module', () => {
it('should return correct default fullLabel for an ImportModule', () => {
const baseMetadata = {
format: 'Foo_test',
description: 'Some description here',
sources: [FileSystemItem.File, FileSystemItem.Directory],
};
const importModuleMultiSource = makeImportModule(
baseMetadata,
() => new InteropService_Importer_Base()
);
const importModuleSingleSource = makeImportModule({
...baseMetadata,
sources: [FileSystemItem.File],
}, () => new InteropService_Importer_Base());
// The two modules should have the same data, except for their sources.
expect(importModuleMultiSource.format).toBe('Foo_test');
expect(importModuleSingleSource.format).toBe(importModuleMultiSource.format);
expect(importModuleMultiSource.sources).toHaveLength(2);
expect(importModuleSingleSource.sources).toHaveLength(1);
const baseLabel = 'FOO - Some description here';
expect(importModuleMultiSource.fullLabel()).toBe(baseLabel);
expect(importModuleSingleSource.fullLabel()).toBe(baseLabel);
// Should only include (File) if the import module has more than one source
expect(importModuleMultiSource.fullLabel(FileSystemItem.File)).toBe(`${baseLabel} (File)`);
expect(importModuleSingleSource.fullLabel(FileSystemItem.File)).toBe(baseLabel);
});
it('should return correct default fullLabel for an ExportModule', () => {
const testExportModule = makeExportModule({
format: 'format_test_______TEST',
description: 'Testing...',
}, () => new InteropService_Exporter_Base());
// Should only include the portion of format before the first underscore
const label = 'FORMAT - Testing...';
expect(testExportModule.fullLabel()).toBe(label);
// Sources should only be shown for import modules
expect(testExportModule.fullLabel(FileSystemItem.File)).toBe(label);
expect(testExportModule.fullLabel(FileSystemItem.Directory)).toBe(label);
});
});

View File

@ -0,0 +1,123 @@
import { _ } from '../../locale';
import InteropService_Exporter_Base from './InteropService_Exporter_Base';
import InteropService_Importer_Base from './InteropService_Importer_Base';
import { ExportOptions, FileSystemItem, ImportModuleOutputFormat, ImportOptions, ModuleType } from './types';
// Metadata shared between importers and exporters.
interface BaseMetadata {
format: string;
fileExtensions: string[];
description: string;
isDefault: boolean;
// Returns the full label to be displayed in the UI.
fullLabel(moduleSource?: FileSystemItem): string;
// Only applies to single file exporters or importers
// It tells whether the format can package multiple notes into one file.
// For example JEX or ENEX can, but HTML cannot.
// Default: true.
isNoteArchive: boolean;
}
interface ImportMetadata extends BaseMetadata {
type: ModuleType.Importer;
sources: FileSystemItem[];
importerClass: string;
outputFormat: ImportModuleOutputFormat;
}
export interface ImportModule extends ImportMetadata {
factory(options?: ImportOptions): InteropService_Importer_Base;
}
interface ExportMetadata extends BaseMetadata {
type: ModuleType.Exporter;
target: FileSystemItem;
}
export interface ExportModule extends ExportMetadata {
factory(options?: ExportOptions): InteropService_Exporter_Base;
}
const defaultBaseMetadata = {
format: '',
fileExtensions: [] as string[],
description: '',
isNoteArchive: true,
isDefault: false,
};
const moduleFullLabel = (metadata: ImportMetadata|ExportMetadata, moduleSource: FileSystemItem = null) => {
const format = metadata.format.split('_')[0];
const label = [`${format.toUpperCase()} - ${metadata.description}`];
if (moduleSource && metadata.type === ModuleType.Importer && metadata.sources.length > 1) {
label.push(`(${moduleSource === FileSystemItem.File ? _('File') : _('Directory')})`);
}
return label.join(' ');
};
export const makeImportModule = (
metadata: Partial<ImportMetadata>, factory: ()=> InteropService_Importer_Base
): ImportModule => {
const importerDefaults: ImportMetadata = {
...defaultBaseMetadata,
type: ModuleType.Importer,
sources: [],
importerClass: '',
outputFormat: ImportModuleOutputFormat.Markdown,
fullLabel: (moduleSource?: FileSystemItem) => {
return moduleFullLabel(fullMetadata, moduleSource);
},
};
const fullMetadata = {
...importerDefaults,
...metadata,
};
return {
...fullMetadata,
factory: (options: ImportOptions = {}) => {
const result = factory();
result.setMetadata({ ...fullMetadata, ...(options ?? {}) });
return result;
},
};
};
export const makeExportModule = (
metadata: Partial<ExportMetadata>, factory: ()=> InteropService_Exporter_Base
): ExportModule => {
const exporterDefaults: ExportMetadata = {
...defaultBaseMetadata,
type: ModuleType.Exporter,
target: FileSystemItem.File,
fullLabel: (moduleSource?: FileSystemItem) => {
return moduleFullLabel(fullMetadata, moduleSource);
},
};
const fullMetadata = {
...exporterDefaults,
...metadata,
};
return {
...fullMetadata,
factory: (options: ExportOptions = {}) => {
const result = factory();
result.setMetadata({ ...fullMetadata, ...(options ?? {}) });
return result;
},
};
};
type Module = ImportModule|ExportModule;
export default Module;

View File

@ -1,4 +1,3 @@
import { _ } from '../../locale';
import { PluginStates } from '../plugins/reducer';
export interface CustomImportContext {
@ -27,58 +26,6 @@ export enum ImportModuleOutputFormat {
Html = 'html',
}
// For historical reasons the import and export modules share the same
// interface, except that some properties are used only for import
// and others only for export.
export interface Module {
// ---------------------------------------
// Shared properties
// ---------------------------------------
type: ModuleType;
format: string;
fileExtensions: string[];
description: string;
// path?: string;
// Only applies to single file exporters or importers
// It tells whether the format can package multiple notes into one file.
// For example JEX or ENEX can, but HTML cannot.
// Default: true.
isNoteArchive?: boolean;
// A custom module is one that was not hard-coded, that was created at runtime
// by a plugin for example. If `isCustom` is `true` if it is expected that all
// the event handlers below are defined (it's enforced by the plugin API).
isCustom?: boolean;
// ---------------------------------------
// Import-only properties
// ---------------------------------------
sources?: FileSystemItem[];
importerClass?: string;
outputFormat?: ImportModuleOutputFormat;
isDefault?: boolean;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
fullLabel?: Function;
// Used only if `isCustom` is true
onExec?(context: any): Promise<any>;
// ---------------------------------------
// Export-only properties
// ---------------------------------------
target?: FileSystemItem;
// Used only if `isCustom` is true
onInit?(context: any): Promise<void>;
onProcessItem?(context: any, itemType: number, item: any): Promise<void>;
onProcessResource?(context: any, resource: any, filePath: string): Promise<void>;
onClose?(context: any): Promise<void>;
}
export interface ImportOptions {
path?: string;
format?: string;
@ -119,29 +66,3 @@ export interface MdFrontMatterExport {
'created'?: string;
'tags'?: string[];
}
function moduleFullLabel(moduleSource: FileSystemItem = null): string {
const format = this.format.split('_')[0];
const label = [`${format.toUpperCase()} - ${this.description}`];
if (moduleSource && this.sources.length > 1) {
label.push(`(${moduleSource === 'file' ? _('File') : _('Directory')})`);
}
return label.join(' ');
}
export function defaultImportExportModule(type: ModuleType): Module {
return {
type: type,
format: '',
fileExtensions: [],
sources: [],
description: '',
isNoteArchive: true,
importerClass: '',
outputFormat: ImportModuleOutputFormat.Markdown,
isDefault: false,
fullLabel: moduleFullLabel,
isCustom: false,
target: FileSystemItem.File,
};
}

View File

@ -1,7 +1,10 @@
/* eslint-disable multiline-comment-style */
import InteropService from '../../interop/InteropService';
import { Module, ModuleType } from '../../interop/types';
import InteropService_Exporter_Custom from '../../interop/InteropService_Exporter_Custom';
import InteropService_Importer_Custom from '../../interop/InteropService_Importer_Custom';
import { makeExportModule, makeImportModule } from '../../interop/Module';
import { ModuleType } from '../../interop/types';
import { ExportModule, ImportModule } from './types';
/**
@ -19,23 +22,23 @@ import { ExportModule, ImportModule } from './types';
export default class JoplinInterop {
public async registerExportModule(module: ExportModule) {
const internalModule: Module = {
const internalModule = makeExportModule({
...module,
type: ModuleType.Exporter,
isCustom: true,
fileExtensions: module.fileExtensions ? module.fileExtensions : [],
};
}, () => new InteropService_Exporter_Custom(module));
return InteropService.instance().registerModule(internalModule);
}
public async registerImportModule(module: ImportModule) {
const internalModule: Module = {
const internalModule = makeImportModule({
...module,
type: ModuleType.Importer,
isCustom: true,
fileExtensions: module.fileExtensions ? module.fileExtensions : [],
};
}, () => {
return new InteropService_Importer_Custom(module);
});
return InteropService.instance().registerModule(internalModule);
}