2021-01-05 20:18:40 +02:00
// -----------------------------------------------------------------------------
// 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.
// -----------------------------------------------------------------------------
2023-02-16 12:55:24 +02:00
/* eslint-disable no-console */
2020-11-15 16:18:46 +02:00
const path = require ( 'path' ) ;
2021-01-04 20:45:43 +02:00
const crypto = require ( 'crypto' ) ;
2020-11-17 20:26:24 +02:00
const fs = require ( 'fs-extra' ) ;
2021-01-04 20:45:43 +02:00
const chalk = require ( 'chalk' ) ;
2020-11-15 16:18:46 +02:00
const CopyPlugin = require ( 'copy-webpack-plugin' ) ;
2020-11-17 20:26:24 +02:00
const tar = require ( 'tar' ) ;
const glob = require ( 'glob' ) ;
2021-01-04 20:45:43 +02:00
const execSync = require ( 'child_process' ) . execSync ;
2022-09-09 16:05:08 +02:00
const allPossibleCategories = require ( '@joplin/lib/pluginCategories.json' ) ;
2021-01-04 20:45:43 +02:00
const rootDir = path . resolve ( _ _dirname ) ;
2021-01-08 18:31:11 +02:00
const userConfigFilename = './plugin.config.json' ;
const userConfigPath = path . resolve ( rootDir , userConfigFilename ) ;
2021-01-04 20:45:43 +02:00
const distDir = path . resolve ( rootDir , 'dist' ) ;
const srcDir = path . resolve ( rootDir , 'src' ) ;
const publishDir = path . resolve ( rootDir , 'publish' ) ;
2023-06-01 13:02:36 +02:00
const userConfig = { extraScripts : [ ] , ... ( fs . pathExistsSync ( userConfigPath ) ? require ( userConfigFilename ) : { } ) } ;
2021-01-08 18:31:11 +02:00
2021-01-04 20:45:43 +02:00
const manifestPath = ` ${ srcDir } /manifest.json ` ;
const packageJsonPath = ` ${ rootDir } /package.json ` ;
2022-08-27 13:11:56 +02:00
const allPossibleScreenshotsType = [ 'jpg' , 'jpeg' , 'png' , 'gif' , 'webp' ] ;
2021-01-04 20:45:43 +02:00
const manifest = readManifest ( manifestPath ) ;
const pluginArchiveFilePath = path . resolve ( publishDir , ` ${ manifest . id } .jpl ` ) ;
const pluginInfoFilePath = path . resolve ( publishDir , ` ${ manifest . id } .json ` ) ;
2022-09-12 11:44:40 +02:00
const { builtinModules } = require ( 'node:module' ) ;
// Webpack5 doesn't polyfill by default and displays a warning when attempting to require() built-in
// node modules. Set these to false to prevent Webpack from warning about not polyfilling these modules.
// We don't need to polyfill because the plugins run in Electron's Node environment.
const moduleFallback = { } ;
for ( const moduleName of builtinModules ) {
moduleFallback [ moduleName ] = false ;
}
2023-06-28 15:48:04 +02:00
const getPackageJson = ( ) => {
return JSON . parse ( fs . readFileSync ( packageJsonPath , 'utf8' ) ) ;
} ;
2021-01-04 20:45:43 +02:00
function validatePackageJson ( ) {
2023-06-28 15:48:04 +02:00
const content = getPackageJson ( ) ;
2021-01-04 20:45:43 +02:00
if ( ! content . name || content . name . indexOf ( 'joplin-plugin-' ) !== 0 ) {
console . warn ( chalk . yellow ( ` WARNING: To publish the plugin, the package name should start with "joplin-plugin-" (found " ${ content . name } ") in ${ packageJsonPath } ` ) ) ;
}
if ( ! content . keywords || content . keywords . indexOf ( 'joplin-plugin' ) < 0 ) {
console . warn ( chalk . yellow ( ` WARNING: To publish the plugin, the package keywords should include "joplin-plugin" (found " ${ JSON . stringify ( content . keywords ) } ") in ${ packageJsonPath } ` ) ) ;
}
if ( content . scripts && content . scripts . postinstall ) {
console . warn ( chalk . yellow ( ` WARNING: package.json contains a "postinstall" script. It is recommended to use a "prepare" script instead so that it is executed before publish. In ${ packageJsonPath } ` ) ) ;
}
}
function fileSha256 ( filePath ) {
const content = fs . readFileSync ( filePath ) ;
return crypto . createHash ( 'sha256' ) . update ( content ) . digest ( 'hex' ) ;
}
function currentGitInfo ( ) {
try {
let branch = execSync ( 'git rev-parse --abbrev-ref HEAD' , { stdio : 'pipe' } ) . toString ( ) . trim ( ) ;
const commit = execSync ( 'git rev-parse HEAD' , { stdio : 'pipe' } ) . toString ( ) . trim ( ) ;
if ( branch === 'HEAD' ) branch = 'master' ;
return ` ${ branch } : ${ commit } ` ;
} catch ( error ) {
const messages = error . message ? error . message . split ( '\n' ) : [ '' ] ;
console . info ( chalk . cyan ( 'Could not get git commit (not a git repo?):' , messages [ 0 ] . trim ( ) ) ) ;
console . info ( chalk . cyan ( 'Git information will not be stored in plugin info file' ) ) ;
return '' ;
}
}
2020-11-17 20:26:24 +02:00
2022-04-10 11:52:31 +02:00
function validateCategories ( categories ) {
if ( ! categories ) return null ;
if ( ( categories . length !== new Set ( categories ) . size ) ) throw new Error ( 'Repeated categories are not allowed' ) ;
2023-06-30 10:39:21 +02:00
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
2022-04-10 11:52:31 +02:00
categories . forEach ( category => {
2022-09-09 16:05:08 +02:00
if ( ! allPossibleCategories . map ( category => { return category . name ; } ) . includes ( category ) ) throw new Error ( ` ${ category } is not a valid category. Please make sure that the category name is lowercase. Valid categories are: \n ${ allPossibleCategories . map ( category => { return category . name ; } )} \n ` ) ;
2022-04-10 11:52:31 +02:00
} ) ;
}
2022-08-27 13:11:56 +02:00
function validateScreenshots ( screenshots ) {
if ( ! screenshots ) return null ;
2023-11-14 20:49:45 +02:00
for ( const screenshot of screenshots ) {
2022-08-27 13:11:56 +02:00
if ( ! screenshot . src ) throw new Error ( 'You must specify a src for each screenshot' ) ;
2023-11-14 20:49:45 +02:00
// Avoid attempting to download and verify URL screenshots.
if ( screenshot . src . startsWith ( 'https://' ) || screenshot . src . startsWith ( 'http://' ) ) {
continue ;
}
2022-08-27 13:11:56 +02:00
const screenshotType = screenshot . src . split ( '.' ) . pop ( ) ;
if ( ! allPossibleScreenshotsType . includes ( screenshotType ) ) throw new Error ( ` ${ screenshotType } is not a valid screenshot type. Valid types are: \n ${ allPossibleScreenshotsType } \n ` ) ;
2023-11-14 20:49:45 +02:00
const screenshotPath = path . resolve ( rootDir , screenshot . src ) ;
2022-08-27 13:11:56 +02:00
// Max file size is 1MB
const fileMaxSize = 1024 ;
const fileSize = fs . statSync ( screenshotPath ) . size / 1024 ;
if ( fileSize > fileMaxSize ) throw new Error ( ` Max screenshot file size is ${ fileMaxSize } KB. ${ screenshotPath } is ${ fileSize } KB ` ) ;
2023-11-14 20:49:45 +02:00
}
2022-08-27 13:11:56 +02:00
}
2020-11-17 20:26:24 +02:00
function readManifest ( manifestPath ) {
const content = fs . readFileSync ( manifestPath , 'utf8' ) ;
const output = JSON . parse ( content ) ;
if ( ! output . id ) throw new Error ( ` Manifest plugin ID is not set in ${ manifestPath } ` ) ;
2022-04-10 11:52:31 +02:00
validateCategories ( output . categories ) ;
2022-08-27 13:11:56 +02:00
validateScreenshots ( output . screenshots ) ;
2020-11-17 20:26:24 +02:00
return output ;
}
function createPluginArchive ( sourceDir , destPath ) {
2023-10-06 19:04:34 +02:00
const distFiles = glob . sync ( ` ${ sourceDir } /**/* ` , { nodir : true , windowsPathsNoEscape : true } )
2020-11-17 20:26:24 +02:00
. map ( f => f . substr ( sourceDir . length + 1 ) ) ;
2021-01-08 18:31:11 +02:00
if ( ! distFiles . length ) throw new Error ( 'Plugin archive was not created because the "dist" directory is empty' ) ;
2020-11-17 20:26:24 +02:00
fs . removeSync ( destPath ) ;
tar . create (
{
strict : true ,
portable : true ,
file : destPath ,
cwd : sourceDir ,
sync : true ,
} ,
2023-08-22 12:58:53 +02:00
distFiles ,
2020-11-17 20:26:24 +02:00
) ;
2021-01-04 20:45:43 +02:00
console . info ( chalk . cyan ( ` Plugin archive has been created in ${ destPath } ` ) ) ;
2020-11-17 20:26:24 +02:00
}
2023-06-28 15:48:04 +02:00
const writeManifest = ( manifestPath , content ) => {
fs . writeFileSync ( manifestPath , JSON . stringify ( content , null , '\t' ) , 'utf8' ) ;
} ;
2021-01-04 20:45:43 +02:00
function createPluginInfo ( manifestPath , destPath , jplFilePath ) {
const contentText = fs . readFileSync ( manifestPath , 'utf8' ) ;
const content = JSON . parse ( contentText ) ;
content . _publish _hash = ` sha256: ${ fileSha256 ( jplFilePath ) } ` ;
content . _publish _commit = currentGitInfo ( ) ;
2023-06-28 15:48:04 +02:00
writeManifest ( destPath , content ) ;
2021-01-04 20:45:43 +02:00
}
2020-11-15 16:18:46 +02:00
2021-01-04 20:45:43 +02:00
function onBuildCompleted ( ) {
2021-01-08 18:31:11 +02:00
try {
2021-01-13 14:16:36 +02:00
fs . removeSync ( path . resolve ( publishDir , 'index.js' ) ) ;
2021-01-08 18:31:11 +02:00
createPluginArchive ( distDir , pluginArchiveFilePath ) ;
createPluginInfo ( manifestPath , pluginInfoFilePath , pluginArchiveFilePath ) ;
validatePackageJson ( ) ;
} catch ( error ) {
console . error ( chalk . red ( error . message ) ) ;
}
2021-01-04 20:45:43 +02:00
}
2020-11-18 14:33:48 +02:00
2021-01-03 15:21:48 +02:00
const baseConfig = {
2020-11-15 16:18:46 +02:00
mode : 'production' ,
target : 'node' ,
2021-01-04 20:45:43 +02:00
stats : 'errors-only' ,
2020-11-15 16:18:46 +02:00
module : {
rules : [
{
test : /\.tsx?$/ ,
use : 'ts-loader' ,
exclude : /node_modules/ ,
} ,
] ,
} ,
2021-01-03 15:21:48 +02:00
} ;
2023-06-01 13:02:36 +02:00
const pluginConfig = { ... baseConfig , entry : './src/index.ts' ,
2020-11-15 16:18:46 +02:00
resolve : {
alias : {
api : path . resolve ( _ _dirname , 'api' ) ,
} ,
2022-09-12 11:44:40 +02:00
fallback : moduleFallback ,
2021-06-12 00:18:33 +02:00
// JSON files can also be required from scripts so we include this.
// https://github.com/joplin/plugin-bibtex/pull/2
2021-12-27 18:39:27 +02:00
extensions : [ '.js' , '.tsx' , '.ts' , '.json' ] ,
2020-11-15 16:18:46 +02:00
} ,
output : {
filename : 'index.js' ,
2020-11-17 20:26:24 +02:00
path : distDir ,
2020-11-15 16:18:46 +02:00
} ,
plugins : [
new CopyPlugin ( {
patterns : [
{
from : '**/*' ,
context : path . resolve ( _ _dirname , 'src' ) ,
to : path . resolve ( _ _dirname , 'dist' ) ,
globOptions : {
ignore : [
2021-01-03 15:21:48 +02:00
// All TypeScript files are compiled to JS and
// already copied into /dist so we don't copy them.
2020-11-15 16:18:46 +02:00
'**/*.ts' ,
'**/*.tsx' ,
] ,
} ,
} ,
] ,
} ) ,
2023-06-01 13:02:36 +02:00
] } ;
2021-01-03 15:21:48 +02:00
2023-12-13 21:45:02 +02:00
// These libraries can be included with require(...) or
// joplin.require(...) from content scripts.
const externalContentScriptLibraries = [
'@codemirror/view' ,
'@codemirror/state' ,
'@codemirror/language' ,
'@codemirror/autocomplete' ,
'@codemirror/commands' ,
'@codemirror/highlight' ,
'@codemirror/lint' ,
'@codemirror/lang-html' ,
2023-12-20 21:10:20 +02:00
'@codemirror/language-data' ,
2023-12-13 21:45:02 +02:00
'@lezer/common' ,
'@lezer/markdown' ,
] ;
const extraScriptExternals = { } ;
for ( const library of externalContentScriptLibraries ) {
extraScriptExternals [ library ] = { commonjs : library } ;
}
const extraScriptConfig = {
... baseConfig ,
resolve : {
alias : {
api : path . resolve ( _ _dirname , 'api' ) ,
} ,
fallback : moduleFallback ,
extensions : [ '.js' , '.tsx' , '.ts' , '.json' ] ,
2021-01-03 15:21:48 +02:00
} ,
2023-12-13 21:45:02 +02:00
// We support requiring @codemirror/... libraries through require('@codemirror/...')
externalsType : 'commonjs' ,
externals : extraScriptExternals ,
} ;
2021-01-03 15:21:48 +02:00
2021-01-13 14:16:36 +02:00
const createArchiveConfig = {
stats : 'errors-only' ,
entry : './dist/index.js' ,
2022-09-12 11:44:40 +02:00
resolve : {
fallback : moduleFallback ,
} ,
2021-01-13 14:16:36 +02:00
output : {
filename : 'index.js' ,
path : publishDir ,
} ,
2022-09-12 11:44:40 +02:00
plugins : [ {
apply ( compiler ) {
compiler . hooks . done . tap ( 'archiveOnBuildListener' , onBuildCompleted ) ;
} ,
} ] ,
2021-01-13 14:16:36 +02:00
} ;
2021-01-08 18:31:11 +02:00
function resolveExtraScriptPath ( name ) {
const relativePath = ` ./src/ ${ name } ` ;
2021-01-03 15:21:48 +02:00
2021-01-08 18:31:11 +02:00
const fullPath = path . resolve ( ` ${ rootDir } / ${ relativePath } ` ) ;
if ( ! fs . pathExistsSync ( fullPath ) ) throw new Error ( ` Could not find extra script: " ${ name } " at " ${ fullPath } " ` ) ;
const s = name . split ( '.' ) ;
s . pop ( ) ;
const nameNoExt = s . join ( '.' ) ;
2021-01-03 15:21:48 +02:00
2021-01-08 18:31:11 +02:00
return {
entry : relativePath ,
output : {
filename : ` ${ nameNoExt } .js ` ,
path : distDir ,
library : 'default' ,
libraryTarget : 'commonjs' ,
libraryExport : 'default' ,
} ,
} ;
2021-01-03 15:21:48 +02:00
}
2021-01-13 14:16:36 +02:00
function buildExtraScriptConfigs ( userConfig ) {
if ( ! userConfig . extraScripts . length ) return [ ] ;
2021-01-03 15:21:48 +02:00
const output = [ ] ;
2021-01-08 18:31:11 +02:00
for ( const scriptName of userConfig . extraScripts ) {
const scriptPaths = resolveExtraScriptPath ( scriptName ) ;
2023-06-01 13:02:36 +02:00
output . push ( { ... extraScriptConfig , entry : scriptPaths . entry ,
output : scriptPaths . output } ) ;
2021-01-03 15:21:48 +02:00
}
2021-01-13 14:16:36 +02:00
return output ;
2021-01-03 15:21:48 +02:00
}
2023-06-28 15:48:04 +02:00
const increaseVersion = version => {
try {
const s = version . split ( '.' ) ;
const d = Number ( s [ s . length - 1 ] ) + 1 ;
s [ s . length - 1 ] = ` ${ d } ` ;
return s . join ( '.' ) ;
} catch ( error ) {
error . message = ` Could not parse version number: ${ version } : ${ error . message } ` ;
throw error ;
}
} ;
const updateVersion = ( ) => {
const packageJson = getPackageJson ( ) ;
packageJson . version = increaseVersion ( packageJson . version ) ;
fs . writeFileSync ( packageJsonPath , ` ${ JSON . stringify ( packageJson , null , 2 ) } \n ` , 'utf8' ) ;
const manifest = readManifest ( manifestPath ) ;
manifest . version = increaseVersion ( manifest . version ) ;
writeManifest ( manifestPath , manifest ) ;
if ( packageJson . version !== manifest . version ) {
console . warn ( chalk . yellow ( ` Version numbers have been updated but they do not match: package.json ( ${ packageJson . version } ), manifest.json ( ${ manifest . version } ). Set them to the required values to get them in sync. ` ) ) ;
}
} ;
2022-09-12 11:44:40 +02:00
function main ( environ ) {
const configName = environ [ 'joplin-plugin-config' ] ;
2021-01-13 14:16:36 +02:00
if ( ! configName ) throw new Error ( 'A config file must be specified via the --joplin-plugin-config flag' ) ;
// Webpack configurations run in parallel, while we need them to run in
// sequence, and to do that it seems the only way is to run webpack multiple
// times, with different config each time.
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
2021-01-15 19:03:38 +02:00
buildMain : [ pluginConfig ] ,
2021-01-13 14:16:36 +02:00
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
// overwritten here by the compiled version. This is by design. The
// result is that JS files that don't need compilation, are simply
// copied to /dist, while those that do need it are correctly compiled.
buildExtraScripts : buildExtraScriptConfigs ( userConfig ) ,
// Ths config is for creating the .jpl, which is done via the plugin, so
// it doesn't actually need an entry and output, however webpack won't
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
2021-01-15 19:03:38 +02:00
createArchive : [ createArchiveConfig ] ,
2021-01-13 14:16:36 +02:00
} ;
// If we are running the first config step, we clean up and create the build
// directories.
if ( configName === 'buildMain' ) {
fs . removeSync ( distDir ) ;
fs . removeSync ( publishDir ) ;
fs . mkdirpSync ( publishDir ) ;
}
2023-06-28 15:48:04 +02:00
if ( configName === 'updateVersion' ) {
updateVersion ( ) ;
return [ ] ;
}
2021-01-13 14:16:36 +02:00
return configs [ configName ] ;
2021-01-08 18:31:11 +02:00
}
2021-01-03 15:21:48 +02:00
2021-01-08 18:31:11 +02:00
2022-09-12 11:44:40 +02:00
module . exports = ( env ) => {
let exportedConfigs = [ ] ;
2021-01-03 15:21:48 +02:00
2022-09-12 11:44:40 +02:00
try {
exportedConfigs = main ( env ) ;
} catch ( error ) {
console . error ( error . message ) ;
process . exit ( 1 ) ;
}
2021-01-14 17:47:34 +02:00
2022-09-12 11:44:40 +02:00
if ( ! exportedConfigs . length ) {
// Nothing to do - for example where there are no external scripts to
// compile.
process . exit ( 0 ) ;
}
return exportedConfigs ;
} ;