mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Plugins: Updated types
This commit is contained in:
parent
c484c88715
commit
41edf5b2da
@ -2,5 +2,6 @@ dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
||||
dist/*
|
||||
*.jpl
|
||||
|
@ -7,3 +7,4 @@
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -55,38 +57,15 @@ In general this command tries to do the right thing - in particular it's going t
|
||||
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -21,7 +21,7 @@ import { Command } from './types';
|
||||
* and look at the `execute()` command.
|
||||
*/
|
||||
export default class JoplinCommands {
|
||||
/**
|
||||
/**
|
||||
* <span class="platform-desktop">desktop</span> Executes the given
|
||||
* command.
|
||||
*
|
||||
@ -40,8 +40,8 @@ export default class JoplinCommands {
|
||||
* await joplin.commands.execute('newFolder', "SOME_FOLDER_ID");
|
||||
* ```
|
||||
*/
|
||||
execute(commandName: string, ...args: any[]): Promise<any | void>;
|
||||
/**
|
||||
execute(commandName: string, ...args: any[]): Promise<any | void>;
|
||||
/**
|
||||
* <span class="platform-desktop">desktop</span> Registers a new command.
|
||||
*
|
||||
* ```typescript
|
||||
@ -57,5 +57,5 @@ export default class JoplinCommands {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
register(command: Command): Promise<void>;
|
||||
register(command: Command): Promise<void>;
|
||||
}
|
||||
|
@ -5,6 +5,6 @@
|
||||
* so for now disable filters.
|
||||
*/
|
||||
export default class JoplinFilters {
|
||||
on(name: string, callback: Function): Promise<void>;
|
||||
off(name: string, callback: Function): Promise<void>;
|
||||
on(name: string, callback: Function): Promise<void>;
|
||||
off(name: string, callback: Function): Promise<void>;
|
||||
}
|
||||
|
@ -12,6 +12,6 @@ import { ExportModule, ImportModule } from './types';
|
||||
* You may also want to refer to the Joplin API documentation to see the list of properties for each item (note, notebook, etc.) - https://joplinapp.org/api/references/rest_api/
|
||||
*/
|
||||
export default class JoplinInterop {
|
||||
registerExportModule(module: ExportModule): Promise<void>;
|
||||
registerImportModule(module: ImportModule): Promise<void>;
|
||||
registerExportModule(module: ExportModule): Promise<void>;
|
||||
registerImportModule(module: ImportModule): Promise<void>;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { Disposable } from './types';
|
||||
declare enum ItemChangeEventType {
|
||||
Create = 1,
|
||||
Update = 2,
|
||||
Delete = 3,
|
||||
Delete = 3
|
||||
}
|
||||
interface ItemChangeEvent {
|
||||
id: string;
|
||||
@ -12,8 +12,8 @@ interface ItemChangeEvent {
|
||||
interface SyncStartEvent {
|
||||
withErrors: boolean;
|
||||
}
|
||||
declare type ItemChangeHandler = (event: ItemChangeEvent)=> void;
|
||||
declare type SyncStartHandler = (event: SyncStartEvent)=> void;
|
||||
declare type ItemChangeHandler = (event: ItemChangeEvent) => void;
|
||||
declare type SyncStartHandler = (event: SyncStartEvent) => void;
|
||||
/**
|
||||
* The workspace service provides access to all the parts of Joplin that
|
||||
* are being worked on - i.e. the currently selected notes or notebooks as
|
||||
|
@ -24,4 +24,4 @@
|
||||
"webpack-cli": "^3.3.11",
|
||||
"chalk": "^4.1.0"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -17,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -76,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -108,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -140,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -156,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -181,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -1,3 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
publish/
|
||||
|
||||
|
@ -6,3 +6,4 @@
|
||||
/dist
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
|
||||
|
@ -29,6 +29,8 @@ The main two files you will want to look at are:
|
||||
- `/src/index.ts`, which contains the entry point for the plugin source code.
|
||||
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
|
||||
|
||||
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
|
||||
|
||||
## Building the plugin
|
||||
|
||||
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
|
||||
@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
|
||||
|
||||
## Updating the plugin framework
|
||||
|
||||
To update the plugin framework, run `yo joplin --update`
|
||||
To update the plugin framework, run `npm run update`.
|
||||
|
||||
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
|
||||
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
|
||||
|
||||
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
|
||||
|
||||
## Content scripts
|
||||
## External script files
|
||||
|
||||
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
|
||||
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
|
||||
|
||||
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
|
||||
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
|
||||
|
||||
For example, assuming these files:
|
||||
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
|
||||
|
||||
```bash
|
||||
/src
|
||||
index.ts # Main plugin script
|
||||
myContentScript.js # One content script (JS)
|
||||
otherContentScript.ts # Another content script (TypeScript)
|
||||
vendor/
|
||||
test.ts # Sub-directories are also supported
|
||||
```
|
||||
|
||||
The `manifest.json` file would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "Testing Content Scripts",
|
||||
content_scripts: [
|
||||
"myContentScript",
|
||||
"otherContentScript",
|
||||
"vendor/test"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note in particular how the file path is relative to /src and the extensions removed.
|
||||
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
|
||||
|
||||
## License
|
||||
|
||||
|
@ -4,9 +4,12 @@
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack",
|
||||
"prepare": "npm run dist"
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
},
|
||||
"keywords": ["joplin-plugin"],
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extraScripts": []
|
||||
}
|
@ -1,3 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
|
||||
// is recommended not to edit this file as it would be overwritten when updating
|
||||
// the plugin framework. If you do make some changes, consider using an external
|
||||
// JS file and requiring it here to minimize the changes. That way when you
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
@ -9,10 +17,16 @@ const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const manifest = readManifest(manifestPath);
|
||||
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) {
|
||||
// Usually means there's an error, which is going to be printed by
|
||||
// webpack
|
||||
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
fs.removeSync(destPath);
|
||||
|
||||
tar.create(
|
||||
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
try {
|
||||
createPluginArchive(distDir, pluginArchiveFilePath);
|
||||
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
|
||||
validatePackageJson();
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
filename: 'index.js',
|
||||
path: distDir,
|
||||
},
|
||||
});
|
||||
|
||||
const lastStepConfig = {
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@ -148,23 +157,15 @@ const lastStepConfig = {
|
||||
// already copied into /dist so we don't copy them.
|
||||
'**/*.ts',
|
||||
'**/*.tsx',
|
||||
|
||||
// Currently we don't support JS files for the main
|
||||
// plugin script. We support it for content scripts,
|
||||
// but they should be declared in manifest.json,
|
||||
// and then they are also compiled and copied to
|
||||
// /dist. So wse also don't need to copy JS files.
|
||||
'**/*.js',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new WebpackOnBuildPlugin(onBuildCompleted),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
@ -173,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
});
|
||||
|
||||
function resolveContentScriptPaths(name) {
|
||||
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
|
||||
throw new Error(`Content script path must not include file extension: ${name}`);
|
||||
}
|
||||
function resolveExtraScriptPath(name) {
|
||||
const relativePath = `./src/${name}`;
|
||||
|
||||
const pathsToTry = [
|
||||
`./src/${name}.ts`,
|
||||
`${'./src/' + '/'}${name}.js`,
|
||||
];
|
||||
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
|
||||
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
|
||||
|
||||
for (const pathToTry of pathsToTry) {
|
||||
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
|
||||
return {
|
||||
entry: pathToTry,
|
||||
output: {
|
||||
filename: `${name}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const s = name.split('.');
|
||||
s.pop();
|
||||
const nameNoExt = s.join('.');
|
||||
|
||||
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
|
||||
return {
|
||||
entry: relativePath,
|
||||
output: {
|
||||
filename: `${nameNoExt}.js`,
|
||||
path: distDir,
|
||||
library: 'default',
|
||||
libraryTarget: 'commonjs',
|
||||
libraryExport: 'default',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createContentScriptConfigs() {
|
||||
if (!manifest.content_scripts) return [];
|
||||
function addExtraScriptConfigs(baseConfig, userConfig) {
|
||||
if (!userConfig.extraScripts.length) return baseConfig;
|
||||
|
||||
const output = [];
|
||||
|
||||
for (const contentScriptName of manifest.content_scripts) {
|
||||
const scriptPaths = resolveContentScriptPaths(contentScriptName);
|
||||
output.push(Object.assign({}, contentScriptConfig, {
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
return baseConfig.concat(output);
|
||||
}
|
||||
|
||||
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
|
||||
function addLastConfigStep(config) {
|
||||
const lastConfig = config[config.length - 1];
|
||||
if (!lastConfig.plugins) lastConfig.plugins = [];
|
||||
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
|
||||
config[config.length - 1] = lastConfig;
|
||||
return config;
|
||||
}
|
||||
|
||||
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
|
||||
let exportedConfigs = [pluginConfig];
|
||||
|
||||
try {
|
||||
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
|
||||
exportedConfigs = addLastConfigStep(exportedConfigs);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
|
@ -34,4 +34,4 @@
|
||||
"repository": "https://github.com/laurent22/generator-joplin",
|
||||
"license": "MIT",
|
||||
"private": true
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user