diff --git a/packages/core/package.json b/packages/core/package.json index b2c86c986c..09235fd8db 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -82,6 +82,7 @@ "@types/jest": "^27.4.1", "@types/js-beautify": "^1.13.3", "@types/lodash": "^4.14.181", + "@types/node": "^18.11.19", "@types/nunjucks": "^3.2.1", "@types/path-is-inside": "^1.0.0", "@types/url-parse": "^1.4.8", diff --git a/packages/core/src/html/NodeProcessor.ts b/packages/core/src/html/NodeProcessor.ts index 2606c82bc3..cb10f72b01 100644 --- a/packages/core/src/html/NodeProcessor.ts +++ b/packages/core/src/html/NodeProcessor.ts @@ -39,6 +39,21 @@ const FRONTMATTER_FENCE = '---'; cheerio.prototype.options.decodeEntities = false; // Don't escape HTML entities +export type NodeProcessorConfig = { + baseUrl: string, + baseUrlMap: Set, + rootPath: string, + outputPath: string, + ignore: string[], + addressablePagesSource: string[], + intrasiteLinkValidation: { enabled: boolean }, + codeLineNumbers: boolean, + plantumlCheck: boolean, + headerIdMap: { + [id: string]: number, + }, +}; + export class NodeProcessor { frontmatter: { [key: string]: string } = {}; @@ -54,20 +69,7 @@ export class NodeProcessor { processedModals: { [id: string]: boolean } = {}; constructor( - private config: { - baseUrl: string, - baseUrlMap: Set, - rootPath: string, - outputPath: string, - ignore: string[], - addressablePagesSource: string[], - intrasiteLinkValidation: { enabled: boolean }, - codeLineNumbers: boolean, - plantumlCheck: boolean, - headerIdMap: { - [id: string]: number, - }, - }, + private config: NodeProcessorConfig, private pageSources: PageSources, private variableProcessor: VariableProcessor, private pluginManager: any, diff --git a/packages/core/src/plugins/Plugin.ts b/packages/core/src/plugins/Plugin.ts index d98b8be35b..e862af94fe 100644 --- a/packages/core/src/plugins/Plugin.ts +++ b/packages/core/src/plugins/Plugin.ts @@ -1,29 +1,66 @@ -const path = require('path'); -const fs = require('fs-extra'); -const cheerio = require('cheerio'); require('../patches/htmlparser2'); +import path from 'path'; +import fs from 'fs-extra'; +import cheerio from 'cheerio'; + +import { DomElement } from 'htmlparser2'; +import isString from 'lodash/isString'; +import * as logger from '../utils/logger'; +import * as urlUtil from '../utils/urlUtil'; +import { NodeProcessorConfig } from '../html/NodeProcessor'; + +require('../patches/htmlparser2'); + +const _ = { isString }; const PLUGIN_OUTPUT_SITE_ASSET_FOLDER_NAME = 'plugins'; -const logger = require('../utils/logger'); -const urlUtil = require('../utils/urlUtil'); +export interface PluginContext { + [key: string]: any; +} + +export interface FrontMatter { + [key: string]: any; +} + +type TagConfigAttributes = { + name: string, + isRelative: boolean, + isSourceFile: boolean +}; + +export type TagConfigs = { + isSpecial: boolean, + attributes: TagConfigAttributes[] +}; /** * Wrapper class around a loaded plugin module */ -class Plugin { - constructor(pluginName, pluginPath, pluginOptions, siteOutputPath) { +export class Plugin { + pluginName: string; + plugin: { + beforeSiteGenerate: (...args: any[]) => any; + getLinks: (...args: any[]) => any; + getScripts: (...args: any[]) => any; + postRender: (pluginContext: PluginContext, frontmatter: FrontMatter, content: string) => string; + processNode: (pluginContext: PluginContext, node: DomElement, config?: NodeProcessorConfig) => string; + postProcessNode: (pluginContext: PluginContext, node: DomElement, config?: NodeProcessorConfig) => string; + tagConfig: { [key: string]: TagConfigs }; + }; + + pluginOptions: PluginContext; + pluginAbsolutePath: string; + pluginAssetOutputPath: string; + + constructor(pluginName: string, pluginPath: string, pluginOptions: PluginContext, siteOutputPath: string) { this.pluginName = pluginName; /** * The plugin module - * @type {Object} */ // eslint-disable-next-line global-require,import/no-dynamic-require this.plugin = require(pluginPath); - /** - * @type {Object} - */ this.pluginOptions = pluginOptions || {}; // For resolving plugin asset source paths later @@ -49,12 +86,12 @@ class Plugin { * @param baseUrl baseUrl of the site * @return String html of the element, with the attribute's asset resolved */ - _getResolvedAssetElement(assetElementHtml, tagName, attrName, baseUrl) { + _getResolvedAssetElement(assetElementHtml: string, tagName: string, attrName: string, baseUrl: string) { const $ = cheerio.load(assetElementHtml); const el = $(`${tagName}[${attrName}]`); - el.attr(attrName, (i, assetPath) => { - if (!assetPath || urlUtil.isUrl(assetPath)) { + el.attr(attrName, (_i: any, assetPath: any): string => { + if (!assetPath || !_.isString(assetPath) || urlUtil.isUrl(assetPath)) { return assetPath; } @@ -79,19 +116,20 @@ class Plugin { /** * Collect page content inserted by plugins */ - getPageNjkLinksAndScripts(frontmatter, content, baseUrl) { + getPageNjkLinksAndScripts(frontmatter: FrontMatter, content: string, baseUrl: string) { let links = []; let scripts = []; if (this.plugin.getLinks) { const pluginLinks = this.plugin.getLinks(this.pluginOptions, frontmatter, content); - links = pluginLinks.map(linkHtml => this._getResolvedAssetElement(linkHtml, 'link', 'href', baseUrl)); + links = pluginLinks.map( + (linkHtml: string) => this._getResolvedAssetElement(linkHtml, 'link', 'href', baseUrl)); } if (this.plugin.getScripts) { const pluginScripts = this.plugin.getScripts(this.pluginOptions, frontmatter, content); - scripts = pluginScripts.map(scriptHtml => this._getResolvedAssetElement(scriptHtml, 'script', - 'src', baseUrl)); + scripts = pluginScripts.map((scriptHtml: string) => this._getResolvedAssetElement(scriptHtml, 'script', + 'src', baseUrl)); } return { @@ -100,14 +138,14 @@ class Plugin { }; } - postRender(frontmatter, content) { + postRender(frontmatter: FrontMatter, content: string) { if (this.plugin.postRender) { return this.plugin.postRender(this.pluginOptions, frontmatter, content); } return content; } - processNode(node, config) { + processNode(node: DomElement, config: NodeProcessorConfig) { if (!this.plugin.processNode) { return; } @@ -115,7 +153,7 @@ class Plugin { this.plugin.processNode(this.pluginOptions, node, config); } - postProcessNode(node, config) { + postProcessNode(node: DomElement, config: NodeProcessorConfig) { if (!this.plugin.postProcessNode) { return; } @@ -127,7 +165,3 @@ class Plugin { return this.plugin.tagConfig; } } - -module.exports = { - Plugin, -}; diff --git a/packages/core/src/plugins/PluginManager.ts b/packages/core/src/plugins/PluginManager.ts index 82076dc448..a35e89ebbe 100644 --- a/packages/core/src/plugins/PluginManager.ts +++ b/packages/core/src/plugins/PluginManager.ts @@ -1,42 +1,60 @@ -const path = require('path'); -const fs = require('fs-extra'); -const walkSync = require('walk-sync'); +import merge from 'lodash/merge'; + +import { DomElement } from 'htmlparser2'; +import path from 'path'; +import fs from 'fs-extra'; +import walkSync from 'walk-sync'; +import flatMap from 'lodash/flatMap'; +import get from 'lodash/get'; +import includes from 'lodash/includes'; +import isError from 'lodash/isError'; +import * as logger from '../utils/logger'; +import { + FrontMatter, Plugin, PluginContext, TagConfigs, +} from './Plugin'; +import { NodeProcessorConfig } from '../html/NodeProcessor'; const { ignoreTags } = require('../patches'); -const { Plugin } = require('./Plugin'); - -const _ = {}; -_.flatMap = require('lodash/flatMap'); -_.get = require('lodash/get'); -_.includes = require('lodash/includes'); -_.merge = require('lodash/merge'); - -const logger = require('../utils/logger'); +const _ = { + flatMap, + get, + includes, + isError, + merge, +}; const MARKBIND_PLUGIN_DIRECTORY = __dirname; const MARKBIND_DEFAULT_PLUGIN_DIRECTORY = path.join(__dirname, 'default'); const MARKBIND_PLUGIN_PREFIX = 'markbind-plugin-'; const PROJECT_PLUGIN_FOLDER_NAME = '_markbind/plugins'; -class PluginManager { - constructor(config, plugins, pluginsContext) { +type PageAsset = { + pluginScripts: string[], + pluginLinks: string[], +}; + +export class PluginManager { + static tagConfig: { [key: string]: TagConfigs }; + + config: NodeProcessorConfig; + plugins: { [key: string]: Plugin }; + pluginsRaw: string[]; + pluginsContextRaw: PluginContext; + htmlBeautifyOptions: { [key: string]: any }; + + constructor(config: NodeProcessorConfig, plugins: string[], pluginsContext: PluginContext) { this.config = config; - /** - * @type {Object} - */ this.plugins = {}; /** * Raw array of plugin names as read from the site configuration - * @type {Array} */ this.pluginsRaw = plugins; /** * Raw representation of the site configuration's plugisnContext key - * @type {Object>} */ this.pluginsContextRaw = pluginsContext; @@ -55,8 +73,6 @@ class PluginManager { * Load all plugins of the site */ _collectPlugins() { - module.paths.push(path.join(this.config.rootPath, 'node_modules')); - const defaultPluginNames = walkSync(MARKBIND_DEFAULT_PLUGIN_DIRECTORY, { directories: false, globs: [`${MARKBIND_PLUGIN_PREFIX}*.js`], @@ -78,7 +94,7 @@ class PluginManager { * @param plugin name of the plugin * @param isDefault whether the plugin is a default plugin */ - _loadPlugin(plugin, isDefault) { + _loadPlugin(plugin: string, isDefault: boolean) { try { // Check if already loaded if (this.plugins[plugin]) { @@ -108,7 +124,7 @@ class PluginManager { * @param projectRootPath root of the MarkBind project * @param pluginName name of the plugin */ - static _getPluginPath(projectRootPath, pluginName) { + static _getPluginPath(projectRootPath: string, pluginName: string) { // Check in project folder const pluginPath = path.join(projectRootPath, PROJECT_PLUGIN_FOLDER_NAME, `${pluginName}.js`); if (fs.existsSync(pluginPath)) { @@ -127,7 +143,19 @@ class PluginManager { return markbindDefaultPluginPath; } - return require.resolve(pluginName); + // Check the environment's node_modules folders + try { + const resolvedPluginPath = require.resolve(pluginName); + return resolvedPluginPath; + } catch (err) { + // An error may be thrown because the module is not found, or for other reasons. + // If the error is due to MODULE_NOT_FOUND, search project's node_modules + if (_.isError(err) && (err as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') { + return require.resolve(pluginName, { paths: [path.join(projectRootPath, 'node_modules')] }); + } + // Re-throw all other errors + throw err; + } } /** @@ -142,7 +170,7 @@ class PluginManager { return; } - Object.entries(pluginTagConfig).forEach(([tagName, tagConfig]) => { + Object.entries(pluginTagConfig).forEach(([tagName, tagConfig]: [string, TagConfigs]) => { if (tagConfig.isSpecial) { specialTags.add(tagName.toLowerCase()); } @@ -168,7 +196,7 @@ class PluginManager { /** * Run getLinks and getScripts hooks */ - collectPluginPageNjkAssets(frontmatter, content, pageAsset) { + collectPluginPageNjkAssets(frontmatter: FrontMatter, content: string, pageAsset: PageAsset) { const pluginLinksAndScripts = Object.values(this.plugins) .map(plugin => plugin.getPageNjkLinksAndScripts(frontmatter, content, this.config.baseUrl)); @@ -176,18 +204,18 @@ class PluginManager { pageAsset.pluginScripts = _.flatMap(pluginLinksAndScripts, pluginResult => pluginResult.scripts); } - postRender(frontmatter, content) { + postRender(frontmatter: FrontMatter, content: string) { return Object.values(this.plugins) .reduce((renderedContent, plugin) => plugin.postRender(frontmatter, renderedContent), content); } - processNode(node) { + processNode(node: DomElement) { Object.values(this.plugins).forEach((plugin) => { plugin.processNode(node, this.config); }); } - postProcessNode(node) { + postProcessNode(node: DomElement) { Object.values(this.plugins).forEach((plugin) => { plugin.postProcessNode(node, this.config); }); @@ -196,7 +224,3 @@ class PluginManager { // Static property for easy access in linkProcessor PluginManager.tagConfig = {}; - -module.exports = { - PluginManager, -}; diff --git a/packages/core/src/plugins/codeBlockCopyButtons.ts b/packages/core/src/plugins/codeBlockCopyButtons.ts index 549d27bfb8..b9b27815ba 100644 --- a/packages/core/src/plugins/codeBlockCopyButtons.ts +++ b/packages/core/src/plugins/codeBlockCopyButtons.ts @@ -5,7 +5,7 @@ import { doesFunctionBtnContainerExistInNode, isFunctionBtnContainer, } from './codeBlockButtonsAssets/codeBlockButtonsContainer'; -import { PluginContext } from './types'; +import { PluginContext } from './Plugin'; const COPY_ICON = `