1
0
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:
Laurent Cozic 2021-01-08 16:36:48 +00:00
parent c484c88715
commit 41edf5b2da
89 changed files with 1159 additions and 1226 deletions

View File

@ -2,5 +2,6 @@ dist/
node_modules/ node_modules/
publish/ publish/
dist/* dist/*
*.jpl *.jpl

View File

@ -7,3 +7,4 @@
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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. 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 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.
/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.
## License ## License

View File

@ -3,7 +3,7 @@ import { Disposable } from './types';
declare enum ItemChangeEventType { declare enum ItemChangeEventType {
Create = 1, Create = 1,
Update = 2, Update = 2,
Delete = 3, Delete = 3
} }
interface ItemChangeEvent { interface ItemChangeEvent {
id: string; id: string;

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -17,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -76,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -108,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -140,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -156,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -181,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -204,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;

View File

@ -1,3 +1,4 @@
dist/ dist/
node_modules/ node_modules/
publish/ publish/

View File

@ -6,3 +6,4 @@
/dist /dist
tsconfig.json tsconfig.json
webpack.config.js webpack.config.js

View File

@ -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/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. - `/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 ## 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. 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 ## 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 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.
/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.
## License ## License

View File

@ -4,9 +4,12 @@
"description": "", "description": "",
"scripts": { "scripts": {
"dist": "webpack", "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", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^14.0.14", "@types/node": "^14.0.14",

View File

@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@ -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 path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -9,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist'); const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src'); const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish'); const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`; const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`; const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath); const manifest = readManifest(manifestPath);
@ -68,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true }) const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1)); .map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) { if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
// 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;
}
fs.removeSync(destPath); fs.removeSync(destPath);
tar.create( tar.create(
@ -100,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
} }
function onBuildCompleted() { function onBuildCompleted() {
try {
createPluginArchive(distDir, pluginArchiveFilePath); createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson(); validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
} }
const baseConfig = { const baseConfig = {
@ -132,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js', filename: 'index.js',
path: distDir, path: distDir,
}, },
});
const lastStepConfig = {
plugins: [ plugins: [
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
@ -148,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them. // already copied into /dist so we don't copy them.
'**/*.ts', '**/*.ts',
'**/*.tsx', '**/*.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: { resolve: {
alias: { alias: {
api: path.resolve(__dirname, 'api'), api: path.resolve(__dirname, 'api'),
@ -173,22 +174,20 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
}, },
}); });
function resolveContentScriptPaths(name) { function resolveExtraScriptPath(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) { const relativePath = `./src/${name}`;
throw new Error(`Content script path must not include file extension: ${name}`);
}
const pathsToTry = [ const fullPath = path.resolve(`${rootDir}/${relativePath}`);
`./src/${name}.ts`, if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
`${'./src/' + '/'}${name}.js`,
]; const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return { return {
entry: pathToTry, entry: relativePath,
output: { output: {
filename: `${name}.js`, filename: `${nameNoExt}.js`,
path: distDir, path: distDir,
library: 'default', library: 'default',
libraryTarget: 'commonjs', libraryTarget: 'commonjs',
@ -196,29 +195,39 @@ function resolveContentScriptPaths(name) {
}, },
}; };
} }
}
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`); function addExtraScriptConfigs(baseConfig, userConfig) {
} if (!userConfig.extraScripts.length) return baseConfig;
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
const output = []; const output = [];
for (const contentScriptName of manifest.content_scripts) { for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName); const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, contentScriptConfig, { output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry, entry: scriptPaths.entry,
output: scriptPaths.output, 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; module.exports = exportedConfigs;