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

Generator: Better handling of external scripts

This commit is contained in:
Laurent Cozic 2021-01-08 16:31:11 +00:00
parent 16788d1437
commit ebbaa5177b
7 changed files with 81 additions and 112 deletions

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/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
## Building the plugin
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
@ -55,38 +57,15 @@ In general this command tries to do the right thing - in particular it's going t
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
## Content scripts
## External script files
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
For example, assuming these files:
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
```bash
/src
index.ts # Main plugin script
myContentScript.js # One content script (JS)
otherContentScript.ts # Another content script (TypeScript)
vendor/
test.ts # Sub-directories are also supported
```
The `manifest.json` file would be:
```json
{
"manifest_version": 1,
"name": "Testing Content Scripts",
content_scripts: [
"myContentScript",
"otherContentScript",
"vendor/test"
]
}
```
Note in particular how the file path is relative to /src and the extensions removed.
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
## License

View File

@ -116,6 +116,7 @@ module.exports = class extends Generator {
'package_TEMPLATE.json',
'tsconfig.json',
'webpack.config.js',
'plugin.config.json',
];
const noUpdateFiles = [
@ -145,6 +146,8 @@ module.exports = class extends Generator {
},
}
);
} else if (this.options.update && destFile === 'plugin.config.json' && this.fs.exists(destFilePath)) {
// Keep existing content for now. Maybe later we could merge the configs.
} else if (this.options.update && (destFile === '.gitignore' || destFile === '.npmignore') && this.fs.exists(destFilePath)) {
const destContent = this.fs.read(destFilePath);

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/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
## Building the plugin
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
@ -55,38 +57,15 @@ In general this command tries to do the right thing - in particular it's going t
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
## Content scripts
## External script files
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
For example, assuming these files:
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
```bash
/src
index.ts # Main plugin script
myContentScript.js # One content script (JS)
otherContentScript.ts # Another content script (TypeScript)
vendor/
test.ts # Sub-directories are also supported
```
The `manifest.json` file would be:
```json
{
"manifest_version": 1,
"name": "Testing Content Scripts",
content_scripts: [
"myContentScript",
"otherContentScript",
"vendor/test"
]
}
```
Note in particular how the file path is relative to /src and the extensions removed.
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
## License

View File

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

View File

@ -17,10 +17,16 @@ const glob = require('glob');
const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath);
@ -76,13 +82,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) {
// Usually means there's an error, which is going to be printed by
// webpack
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
return;
}
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
fs.removeSync(destPath);
tar.create(
@ -108,9 +108,13 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
}
function onBuildCompleted() {
createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson();
try {
createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
}
const baseConfig = {
@ -140,9 +144,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js',
path: distDir,
},
});
const lastStepConfig = {
plugins: [
new CopyPlugin({
patterns: [
@ -156,23 +157,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them.
'**/*.ts',
'**/*.tsx',
// Currently we don't support JS files for the main
// plugin script. We support it for content scripts,
// but they should be declared in manifest.json,
// and then they are also compiled and copied to
// /dist. So wse also don't need to copy JS files.
'**/*.js',
],
},
},
],
}),
new WebpackOnBuildPlugin(onBuildCompleted),
],
};
});
const contentScriptConfig = Object.assign({}, baseConfig, {
const extraScriptConfig = Object.assign({}, baseConfig, {
resolve: {
alias: {
api: path.resolve(__dirname, 'api'),
@ -181,52 +174,60 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
},
});
function resolveContentScriptPaths(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
throw new Error(`Content script path must not include file extension: ${name}`);
}
function resolveExtraScriptPath(name) {
const relativePath = `./src/${name}`;
const pathsToTry = [
`./src/${name}.ts`,
`${'./src/' + '/'}${name}.js`,
];
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return {
entry: pathToTry,
output: {
filename: `${name}.js`,
path: distDir,
library: 'default',
libraryTarget: 'commonjs',
libraryExport: 'default',
},
};
}
}
const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
return {
entry: relativePath,
output: {
filename: `${nameNoExt}.js`,
path: distDir,
library: 'default',
libraryTarget: 'commonjs',
libraryExport: 'default',
},
};
}
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
function addExtraScriptConfigs(baseConfig, userConfig) {
if (!userConfig.extraScripts.length) return baseConfig;
const output = [];
for (const contentScriptName of manifest.content_scripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName);
output.push(Object.assign({}, contentScriptConfig, {
for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry,
output: scriptPaths.output,
}));
}
return output;
return baseConfig.concat(output);
}
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
function addLastConfigStep(config) {
const lastConfig = config[config.length - 1];
if (!lastConfig.plugins) lastConfig.plugins = [];
lastConfig.plugins.push(new WebpackOnBuildPlugin(onBuildCompleted));
config[config.length - 1] = lastConfig;
return config;
}
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
let exportedConfigs = [pluginConfig];
try {
exportedConfigs = addExtraScriptConfigs(exportedConfigs, userConfig);
exportedConfigs = addLastConfigStep(exportedConfigs);
} catch (error) {
console.error(chalk.red(error.message));
process.exit(1);
}
module.exports = exportedConfigs;

View File

@ -1,5 +1,7 @@
const slugify = require('slugify');
// "source" is the framework current version.
// "dest" is the user existing version.
function mergePackageKey(parentKey, source, dest) {
const output = Object.assign({}, dest);
@ -16,7 +18,10 @@ function mergePackageKey(parentKey, source, dest) {
output[k] = source[k];
} else if (parentKey === 'devDependencies') {
// If we are dealing with the dependencies, overwrite with the
// version from source.
// version from source. Note that it can be a problem if the user
// prefers a specific package version but it is hard to avoid,
// otherwise it's not possible to upgrade the dependencies when the
// framework changes.
output[k] = source[k];
} else if (typeof source[k] === 'object' && !Array.isArray(source[k]) && source[k] !== null) {
// If it's an object, recursively process it

View File

@ -11,7 +11,6 @@ Name | Type | Required? | Description
`description` | string | No | Detailed description of the plugin.
`author` | string | No | Plugin author name.
`homepage_url` | string | No | Homepage URL of the plugin. It can also be, for example, a link to a GitHub repository.
`content_scripts` | string[] | No | List of [content scripts](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md#content-scripts) used by the plugin.
## Manifest example