diff --git a/.editorconfig b/.editorconfig index e89330a61..5dc633cf7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,8 @@ indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true +max_line_length = 132 + [*.md] max_line_length = off diff --git a/libs/scully-schematics/src/add-plugin/index.ts b/libs/scully-schematics/src/add-plugin/index.ts index 45a924869..8324273b5 100644 --- a/libs/scully-schematics/src/add-plugin/index.ts +++ b/libs/scully-schematics/src/add-plugin/index.ts @@ -1,12 +1,4 @@ -import { - Rule, - Tree, - url, - applyTemplates, - move, - chain, - SchematicContext -} from '@angular-devkit/schematics'; +import { Rule, Tree, url, applyTemplates, move, chain, SchematicContext } from '@angular-devkit/schematics'; import { strings, normalize } from '@angular-devkit/core'; import { Schema } from './schema'; import { applyWithOverwrite, getRoot, getScullyConfig } from '../utils/utils'; @@ -15,36 +7,23 @@ export default (options: Schema): Rule => { return chain([addPlugin(options), registerPlugin(options)]); }; -const addPlugin = (options: Schema) => ( - tree: Tree, - context: SchematicContext -) => { +const addPlugin = (options: Schema) => (tree: Tree, context: SchematicContext) => { const sourceRoot = getRoot(tree, options.project); - const pathName = strings.dasherize( - `${sourceRoot}/scully-plugins/` - ); + const pathName = strings.dasherize(`${sourceRoot}/scully-plugins/`); return applyWithOverwrite(url(`../files/plugin/${options.pluginType}`), [ applyTemplates({ classify: strings.classify, dasherize: strings.dasherize, camelize: strings.camelize, - name: options.name + name: options.name, }), - move(normalize(pathName)) + move(normalize(pathName)), ]); }; -const registerPlugin = (options: Schema) => ( - tree: Tree, - context: SchematicContext -) => { +const registerPlugin = (options: Schema) => (tree: Tree, context: SchematicContext) => { const scullyConfigFile = getScullyConfig(tree, options.project); - let scullyConfig = tree - .read(`${getRoot(tree, options.project)}/${scullyConfigFile}`) - .toString(); + let scullyConfig = tree.read(`${getRoot(tree, options.project)}/${scullyConfigFile}`).toString(); scullyConfig = `require('./scully-plugins/${strings.dasherize(options.name)}.plugin.js');\n${scullyConfig}`; - tree.overwrite( - `${getRoot(tree, options.project)}/${scullyConfigFile}`, - scullyConfig - ); + tree.overwrite(`${getRoot(tree, options.project)}/${scullyConfigFile}`, scullyConfig); }; diff --git a/libs/scully/src/index.ts b/libs/scully/src/index.ts index 15c3a873a..a15e71c26 100644 --- a/libs/scully/src/index.ts +++ b/libs/scully/src/index.ts @@ -1,22 +1,9 @@ -import { - getConfig, - getPluginConfig, - setConfig, - setPluginConfig, - findPlugin, -} from './lib/pluginManagement/pluginConfig'; -import { - configValidator, - registerPlugin, -} from './lib/pluginManagement/pluginRepository'; +import { getConfig, getPluginConfig, setConfig, setPluginConfig, findPlugin } from './lib/pluginManagement/pluginConfig'; +import { configValidator, registerPlugin } from './lib/pluginManagement/pluginRepository'; import './lib/pluginManagement/systemPlugins'; import { ContentMetaData } from './lib/renderPlugins/content-render-utils/readFileAndCheckPrePublishSlug'; -import { HandledRoute } from './lib/routerPlugins/addOptionalRoutesPlugin'; -import { - scullyConfig, - updateScullyConfig, - loadConfig, -} from './lib/utils/config'; +import { HandledRoute } from './lib/routerPlugins/handledRoute.interface'; +import { scullyConfig, updateScullyConfig, loadConfig } from './lib/utils/config'; import { httpGetJson } from './lib/utils/httpGetJson'; import { RouteTypes, ScullyConfig } from './lib/utils/interfacesandenums'; import { replaceFirstRouteParamWithVal } from './lib/utils/replaceFirstRouteParamWithVal'; diff --git a/libs/scully/src/lib/pluginManagement/Plugin.interfaces.ts b/libs/scully/src/lib/pluginManagement/Plugin.interfaces.ts new file mode 100644 index 000000000..b90e9703f --- /dev/null +++ b/libs/scully/src/lib/pluginManagement/Plugin.interfaces.ts @@ -0,0 +1,49 @@ +import { HandledRoute } from '../routerPlugins/handledRoute.interface'; +import { scullySystem } from './pluginRepository'; +export type ScullySystem = `___Scully_system_plugins_Alter_at_own_RISK___`; + +export type ErrorString = string; +export type ConfigValidator = (HandledRoute) => ErrorString[] | Promise; +type RoutePlugin = (route: string, config: any) => Promise; +type RenderPlugin = (html: string, route: HandledRoute) => Promise; +export type RouteProcess = (routes: HandledRoute[]) => Promise; +type RouteDiscoveryPlugin = (routes: HandledRoute[]) => Promise; +type AllDonePlugin = (routes: HandledRoute[]) => Promise; +export type FilePlugin = (html: string, route: HandledRoute) => Promise; +export interface Plugins { + render: { [name: string]: RenderPlugin }; + router: { [name: string]: RoutePlugin }; + routeProcess: { [name: string]: RouteProcess }; + routeDiscoveryDone: { [name: string]: RouteDiscoveryPlugin }; + allDone: { [name: string]: AllDonePlugin }; + fileHandler: { [fileExtension: string]: FilePlugin }; + [scullySystem]: { [pluginSymbol: string]: (...args: unknown[]) => unknown }; +} + +export type PluginTypes = keyof Plugins; + +export type PluginFunction = (...args: unknown[]) => unknown; +export interface RegisterOptions { + replaceExistingPlugin?: boolean; +} +// Function overloads for registerPlugin to provide better/cleaner autocomplete help. +//TODO add jsondoc signatures on every type. +export interface Register { + ( + type: 'router', + name: string | symbol, + plugin: PluginFunction, + validator?: ConfigValidator, + registerOptions?: RegisterOptions + ): void; + ( + type: 'render' | 'allDone' | 'routeDiscoveryDone', + name: string | symbol, + plugin: PluginFunction, + dummy?, + registerOptions?: RegisterOptions + ): void; + (type: ScullySystem, name: symbol, plugin: PluginFunction, dummy?, registerOptions?: RegisterOptions): void; + (type: 'routeProcess', name: string | symbol, plugin: PluginFunction, priority?: number, registerOptions?: RegisterOptions): void; + (type: 'fileHandler', name: string, plugin: PluginFunction, additionalTypes?: string[], registerOptions?: RegisterOptions): void; +} diff --git a/libs/scully/src/lib/pluginManagement/index.ts b/libs/scully/src/lib/pluginManagement/index.ts index 33f7d1353..90a1784ce 100644 --- a/libs/scully/src/lib/pluginManagement/index.ts +++ b/libs/scully/src/lib/pluginManagement/index.ts @@ -1,7 +1,3 @@ -export { - registerPlugin, - configValidator, - scullySystem, -} from './pluginRepository'; - -export { findPlugin } from './pluginConfig'; +export * from './pluginRepository'; +export * from './pluginConfig'; +export * from './Plugin.interfaces'; diff --git a/libs/scully/src/lib/pluginManagement/pluginConfig.ts b/libs/scully/src/lib/pluginManagement/pluginConfig.ts index a49e9bbd2..107fdb4f0 100644 --- a/libs/scully/src/lib/pluginManagement/pluginConfig.ts +++ b/libs/scully/src/lib/pluginManagement/pluginConfig.ts @@ -4,29 +4,25 @@ // tslint:disable: no-shadowed-variable import { Serializable } from 'puppeteer'; import { logError, yellow } from '../utils/log'; -import { - accessPluginDirectly, - configData, - plugins, - PluginTypes, - pluginTypes, -} from './pluginRepository'; +import { PluginFunction, PluginTypes } from './Plugin.interfaces'; +import { accessPluginDirectly, configData, plugins, pluginTypes } from './pluginRepository'; export const backupData = configData + 'BackupData__'; export const routeConfigData = configData + 'Route_Config_Data__'; export const resetConfig = configData + 'resetData__'; -export const setPluginConfig = ( - name: string | symbol, +interface SetPluginConfig { + (name: string | symbol, configData: Serializable): void; + (name: string | symbol, type: PluginTypes, configData: Serializable): void; +} +export const setPluginConfig: SetPluginConfig = ( + name: string, typeOrConfig: PluginTypes | Serializable, config?: Serializable ): void => { let type: string; // tslint:disable-next-line: no-angle-bracket-type-assertion - if ( - (typeof typeOrConfig === 'string' || typeof typeOrConfig === 'symbol') && - pluginTypes.includes(typeOrConfig) - ) { + if ((typeof typeOrConfig === 'string' || typeof typeOrConfig === 'symbol') && pluginTypes.includes(typeOrConfig)) { type = typeOrConfig; } else { config = (typeOrConfig as unknown) as Serializable; @@ -48,37 +44,22 @@ export function fetchPlugins(name: string | symbol, type?: string): Function[] { return result; } -export function findPlugin( - name: string | symbol, - type?: string, - errorOnNotfound = true -): Function { +export function findPlugin(name: string | symbol, type?: string, errorOnNotfound = true): Function { const found = fetchPlugins(name, type); const displayName = typeof name === 'string' ? name : name.description; switch (found.length) { case 0: if (errorOnNotfound) { - logError( - `Plugin "${yellow(displayName)}" of type "${yellow( - type - )}" is not found, can not store config` - ); + logError(`Plugin "${yellow(displayName)}" of type "${yellow(type)}" is not found, can not store config`); process.exit(15); } return undefined; break; case 1: - const pl = found[0] as Function; - return pl.hasOwnProperty(accessPluginDirectly) - ? (pl[accessPluginDirectly] as Function) - : pl; + return found[0] as PluginFunction; default: if (errorOnNotfound) { - logError( - `Plugin "${yellow( - displayName - )}" has multiple types, please specify type to be able to store config` - ); + logError(`Plugin "${yellow(displayName)}" has multiple types, please specify type to be able to store config`); process.exit(15); } return undefined; @@ -89,11 +70,15 @@ export function hasPlugin(name: string | symbol, type?: string): boolean { return fetchPlugins(name, type).length === 1; } -export const getConfig = (plugin: any): T => (plugin[configData] || {}) as T; +export const getConfig = (plugin: any): T => { + const target = plugin.hasOwnProperty(accessPluginDirectly) ? plugin[accessPluginDirectly] : plugin; + return target[configData] || ({} as T); +}; export const setConfig = (plugin: any, config: Serializable): void => { - plugin[configData] = Object.assign({}, plugin[configData] || {}, config); - plugin[backupData] = { ...plugin[configData] }; + const target = plugin.hasOwnProperty(accessPluginDirectly) ? plugin[accessPluginDirectly] : plugin; + target[configData] = Object.assign({}, target[configData] || {}, config); + target[backupData] = { ...target[configData] }; }; /** @@ -106,24 +91,17 @@ export const routePluginConfig = ( name: string, typeOrConfig: PluginTypes | Serializable, config?: Serializable -) => { +): void => { let type: string; // tslint:disable-next-line: no-angle-bracket-type-assertion - if ( - (typeof typeOrConfig === 'string' || typeof typeOrConfig === 'symbol') && - pluginTypes.includes(typeOrConfig) - ) { + if ((typeof typeOrConfig === 'string' || typeof typeOrConfig === 'symbol') && pluginTypes.includes(typeOrConfig)) { type = typeOrConfig; } else { config = (typeOrConfig as unknown) as Serializable; } const plugin = findPlugin(name, type); plugin[routeConfigData] = plugin[routeConfigData] || {}; - plugin[routeConfigData][route] = Object.assign( - {}, - plugin[configData] || {}, - config - ); + plugin[routeConfigData][route] = Object.assign({}, plugin[configData] || {}, config); plugin[resetConfig] = () => { plugin[configData] = plugin[backupData]; }; diff --git a/libs/scully/src/lib/pluginManagement/pluginRepository.ts b/libs/scully/src/lib/pluginManagement/pluginRepository.ts index cf5a71589..b2c6d3cf0 100644 --- a/libs/scully/src/lib/pluginManagement/pluginRepository.ts +++ b/libs/scully/src/lib/pluginManagement/pluginRepository.ts @@ -1,5 +1,5 @@ -import { HandledRoute } from '../routerPlugins/addOptionalRoutesPlugin'; -import { logError, yellow } from '../utils/log'; +import { yellow } from '../utils/log'; +import { ConfigValidator, PluginFunction, Plugins, Register, RegisterOptions, PluginTypes } from './Plugin.interfaces'; import { hasPlugin } from './pluginConfig'; import { wrap } from './pluginWrap'; @@ -8,96 +8,67 @@ export const configValidator = `___Scully_Validate_config_plugin___`; export const configData = `___Scully_config_for_plugin___`; export const AlternateExtensionsForFilePlugin = Symbol('altfileextension'); export const accessPluginDirectly = Symbol('accessPluginDirectly'); +export const routeProcessPriority = Symbol('routeProcessPriority'); export const scullySystem = `___Scully_system_plugins_Alter_at_own_RISK___`; - -export type ErrorString = string; -export type ConfigValidator = (HandledRoute) => ErrorString[]; - -type RoutePlugin = (route: string, config: any) => Promise; -type RenderPlugin = (html: string, route: HandledRoute) => Promise; -type RouteDiscoveryPlugin = (routes: HandledRoute[]) => Promise; -type AllDonePlugin = (routes: HandledRoute[]) => Promise; -export type FilePlugin = (html: string, route: HandledRoute) => Promise; - -interface Plugins { - render: { [name: string]: RenderPlugin }; - router: { [name: string]: RoutePlugin }; - routeDiscoveryDone: { [name: string]: RouteDiscoveryPlugin }; - allDone: { [name: string]: AllDonePlugin }; - fileHandler: { [fileExtension: string]: FilePlugin }; - [scullySystem]: { [pluginSymbol: string]: Function }; -} - export const plugins: Plugins = { render: {}, router: {}, fileHandler: {}, + routeProcess: {}, routeDiscoveryDone: {}, allDone: {}, [scullySystem]: {}, }; -export type PluginTypes = keyof Plugins; export const pluginTypes = [ 'router', 'render', + 'routeProcess', 'fileHandler', 'allDone', 'routeDiscoveryDone', scullySystem, ] as const; +/** type helpers for registerPlugin */ + // eslint-disable @typescript-eslint/no-explicit-any -export const registerPlugin = ( +export const registerPlugin: Register = ( type: PluginTypes, name: string | symbol, - plugin: any, - pluginOptions: any = async (config?: any) => [], - { replaceExistingPlugin = false } = {} + plugin: PluginFunction, + pluginOptions?: ConfigValidator | number | string[], + { replaceExistingPlugin = false }: RegisterOptions = {} ): void => { const displayName = typeof name === 'string' ? name : name.description; if (!pluginTypes.includes(type)) { throw new Error(` --------------- - Type "${yellow( - type - )}" is not a known plugin type for registering plugin "${yellow(name)}". +---------------------------------------------------------------------------------------------- + Type "${yellow(type)}" is not a known plugin type for registering plugin "${yellow(name)}". The first parameter of registerPlugin needs to be one of: - 'fileHandler', 'router', 'render', 'allDone', or 'routeDiscoveryDone' --------------- + 'fileHandler', 'router', 'render', 'routeProcess', 'allDone', or 'routeDiscoveryDone' +---------------------------------------------------------------------------------------------- `); } if (replaceExistingPlugin === false && hasPlugin(name, type)) { throw new Error(`Plugin ${displayName} already exists`); } - if (type === 'router') { - if (typeof pluginOptions !== 'function') { - logError(` ---------------- - Route plugin "${yellow( - displayName - )}" validator needs to be of type function not "${yellow( - typeof pluginOptions - )}"' ---------------- -`); - pluginOptions = async () => []; - } - } - if (type === 'fileHandler') { - if (pluginOptions && Array.isArray(pluginOptions)) { - plugin[AlternateExtensionsForFilePlugin] = pluginOptions; - } else { - plugin[AlternateExtensionsForFilePlugin] = []; - } + switch (type) { + case 'router': + plugin[configValidator] = typeof pluginOptions === 'function' ? pluginOptions : () => [] as string[]; + break; + case 'fileHandler': + plugin[AlternateExtensionsForFilePlugin] = Array.isArray(pluginOptions) ? pluginOptions : []; + break; + case 'routeProcess': + plugin[routeProcessPriority] = typeof pluginOptions === 'number' ? pluginOptions : 100; + break; } + const wrapper = (...args) => wrap(type, name, plugin, args); /** keep a reference for future use. */ wrapper[accessPluginDirectly] = plugin; - if (type === 'router') { - plugin[configValidator] = pluginOptions; - wrapper[configValidator] = pluginOptions; - } + // plugins[type][name] = wrapper; Object.assign(plugins[type], plugins[type], { [name]: wrapper }); }; diff --git a/libs/scully/src/lib/pluginManagement/pluginWrap.ts b/libs/scully/src/lib/pluginManagement/pluginWrap.ts index 6a4a99a2c..4fd736dba 100644 --- a/libs/scully/src/lib/pluginManagement/pluginWrap.ts +++ b/libs/scully/src/lib/pluginManagement/pluginWrap.ts @@ -4,7 +4,8 @@ import { pluginsError } from '../utils/cli-options'; import { logError, yellow, logWrite } from '../utils/log'; import { performanceIds } from '../utils/performanceIds'; import { backupData, routeConfigData } from './pluginConfig'; -import { configData, FilePlugin } from './pluginRepository'; +import { configData } from './pluginRepository'; +import { FilePlugin } from './Plugin.interfaces'; let typeId = 0; /** @@ -15,12 +16,7 @@ let typeId = 0; * @param args */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export async function wrap( - type: string, - name: string | symbol, - plugin: (...args) => any | FilePlugin, - args: any -): Promise { +export async function wrap(type: string, name: string | symbol, plugin: (...args) => any | FilePlugin, args: any): Promise { const displayName = typeof name === 'string' ? name : name.description; let id = `plugin-${type}:${displayName}-`; @@ -51,8 +47,8 @@ export async function wrap( } catch (e) { logError( ` The ${type} plugin "${yellow(displayName)} has thrown the below error, - while trying to render route "${yellow(currentRoute || 'unknown')}" - ${pluginsError ? 'Scully will exit' : 'Results are ignored.'}` + while trying to render route "${yellow(currentRoute || 'unknown')}" + ${pluginsError ? 'Scully will exit' : 'Results are ignored.'}` ); logWrite(e); if (pluginsError) { diff --git a/libs/scully/src/lib/renderPlugins/contentRenderPlugin.ts b/libs/scully/src/lib/renderPlugins/contentRenderPlugin.ts index 8242380e3..b44bbd958 100644 --- a/libs/scully/src/lib/renderPlugins/contentRenderPlugin.ts +++ b/libs/scully/src/lib/renderPlugins/contentRenderPlugin.ts @@ -1,6 +1,6 @@ import { JSDOM } from 'jsdom'; import { registerPlugin } from '../pluginManagement/pluginRepository'; -import { HandledRoute } from '../routerPlugins/addOptionalRoutesPlugin'; +import { HandledRoute } from '../routerPlugins/handledRoute.interface'; import { ssl } from '../utils'; import { logWarn, yellow } from '../utils/log'; import { getScript } from './content-render-utils/getScript'; @@ -19,23 +19,15 @@ export async function contentRenderPlugin(html: string, route: HandledRoute) { try { let attr = ''; try { - attr = getIdAttrName( - html.split('')[0].trim() - ); + attr = getIdAttrName(html.split('')[0].trim()); } catch (e) { logWarn(` ---------------- - Error, missing "${yellow('')}" in route "${yellow( - route.route - )}" + Error, missing "${yellow('')}" in route "${yellow(route.route)}" without we can not render this route. Make sure it is in there, and not inside any conditionals (*ngIf) - You can check this by opening "${yellow( - `http${ssl ? 'S' : ''}://localhost:4200/${route.route}` - )}" - when you serve your app with ${yellow( - 'ng serve' - )} and then in the browsers console run: + You can check this by opening "${yellow(`http${ssl ? 'S' : ''}://localhost:4200/${route.route}`)}" + when you serve your app with ${yellow('ng serve')} and then in the browsers console run: ${yellow(`document.querySelector('scully-content')`)} ---------------- `); @@ -44,30 +36,14 @@ export async function contentRenderPlugin(html: string, route: HandledRoute) { try { const extension = file.split('.').pop(); const { fileContent } = await readFileAndCheckPrePublishSlug(file); - additionalHTML = await customMarkdownOptions( - await contentToHTML(extension, fileContent, route) - ); + additionalHTML = await customMarkdownOptions(await contentToHTML(extension, fileContent, route)); } catch (e) { - logWarn( - `Error, while reading content for "${yellow( - route.route - )}" from file: "${yellow(file)}"` - ); + logWarn(`Error, while reading content for "${yellow(route.route)}" from file: "${yellow(file)}"`); } const htmlWithNgAttr = addNgIdAttribute(additionalHTML, attr); - return insertContent( - scullyBegin, - scullyEnd, - html, - htmlWithNgAttr, - getScript(attr) - ); + return insertContent(scullyBegin, scullyEnd, html, htmlWithNgAttr, getScript(attr)); } catch (e) { - logWarn( - `Error, while rendering content for "${yellow( - route.route - )}" from file: "${yellow(file)}"` - ); + logWarn(`Error, while rendering content for "${yellow(route.route)}" from file: "${yellow(file)}"`); console.error(e); } } diff --git a/libs/scully/src/lib/renderPlugins/executePlugins.ts b/libs/scully/src/lib/renderPlugins/executePlugins.ts index c7a70a9d5..4e926674c 100644 --- a/libs/scully/src/lib/renderPlugins/executePlugins.ts +++ b/libs/scully/src/lib/renderPlugins/executePlugins.ts @@ -1,9 +1,6 @@ -import { - registerPlugin, - scullySystem, -} from '../pluginManagement/pluginRepository'; import { findPlugin } from '../pluginManagement/pluginConfig'; -import { HandledRoute } from '../routerPlugins/addOptionalRoutesPlugin'; +import { registerPlugin, scullySystem } from '../pluginManagement/pluginRepository'; +import { HandledRoute } from '../routerPlugins/handledRoute.interface'; import { scullyConfig } from '../utils/config'; import { logError, yellow } from '../utils/log'; import { puppeteerRender } from './puppeteerRenderPlugin'; @@ -12,28 +9,17 @@ export const renderRoute = Symbol('renderRoute'); const executePluginsForRoute = async (route: HandledRoute) => { /** make one array with all handlers for this route, filter out empty ones */ - const handlers = [ - route.type, - ...(route.postRenderers || scullyConfig.defaultPostRenderers), - ].filter(Boolean); + const handlers = [route.type, ...(route.postRenderers || scullyConfig.defaultPostRenderers)].filter(Boolean); const preRender = route.config && route.config.preRenderer; if (preRender) { try { const prResult = await preRender(route); if (prResult === false) { - logError( - `prerender stopped rendering for "${yellow( - route.route - )}". This route is skipped.` - ); + logError(`prerender stopped rendering for "${yellow(route.route)}". This route is skipped.`); return ''; } } catch (e) { - logError( - `prerender trowed during rendering for "${yellow( - route.route - )}". This route is skipped.` - ); + logError(`prerender trowed during rendering for "${yellow(route.route)}". This route is skipped.`); /** abort when prerender throws */ return ''; } @@ -48,9 +34,9 @@ const executePluginsForRoute = async (route: HandledRoute) => { return await handler(html, route); } catch { logError( - `Error during content generation with plugin "${yellow( - plugin - )}" for ${yellow(route.templateFile)}. This hander is skipped.` + `Error during content generation with plugin "${yellow(plugin)}" for ${yellow( + route.templateFile + )}. This hander is skipped.` ); } } diff --git a/libs/scully/src/lib/renderPlugins/puppeteerRenderPlugin.ts b/libs/scully/src/lib/renderPlugins/puppeteerRenderPlugin.ts index 3c6f71106..a4b6f3e1f 100644 --- a/libs/scully/src/lib/renderPlugins/puppeteerRenderPlugin.ts +++ b/libs/scully/src/lib/renderPlugins/puppeteerRenderPlugin.ts @@ -6,7 +6,7 @@ import { join } from 'path'; import { Browser, Page, Serializable } from 'puppeteer'; import { interval, Subject } from 'rxjs'; import { filter, switchMap, take } from 'rxjs/operators'; -import { HandledRoute } from '../routerPlugins/addOptionalRoutesPlugin'; +import { HandledRoute } from '../routerPlugins/handledRoute.interface'; import { createFolderFor } from '../utils'; import { ssl, showBrowser } from '../utils/cli-options'; import { scullyConfig } from '../utils/config'; @@ -34,9 +34,7 @@ const plugin = async (route: HandledRoute): Promise => { const pageLoaded = new Subject(); const path = scullyConfig.hostUrl ? `${scullyConfig.hostUrl}${route.route}` - : `http${ssl ? 's' : ''}://${scullyConfig.hostName}:${ - scullyConfig.appPort - }${route.route}`; + : `http${ssl ? 's' : ''}://${scullyConfig.hostName}:${scullyConfig.appPort}${route.route}`; let pageHtml: string; let browser: Browser; let page: Page; @@ -88,10 +86,7 @@ const plugin = async (route: HandledRoute): Promise => { }); } - if ( - scullyConfig.ignoreResourceTypes && - scullyConfig.ignoreResourceTypes.length > 0 - ) { + if (scullyConfig.ignoreResourceTypes && scullyConfig.ignoreResourceTypes.length > 0) { await page.setRequestInterception(true); page.on('request', checkIfRequestShouldBeIgnored); @@ -153,9 +148,7 @@ const plugin = async (route: HandledRoute): Promise => { d.innerHTML = `window['ScullyIO']='generated';`; if (window['ScullyIO-injected']) { /** and add the injected data there too. */ - d.innerHTML += `window['ScullyIO-injected']=${JSON.stringify( - window['ScullyIO-injected'] - )};`; + d.innerHTML += `window['ScullyIO-injected']=${JSON.stringify(window['ScullyIO-injected'])};`; } const m = document.createElement('meta'); m.name = 'generator'; @@ -208,11 +201,7 @@ const plugin = async (route: HandledRoute): Promise => { const { message } = err; // tslint:disable-next-line: no-unused-expression page && typeof page.close === 'function' && (await page.close()); - logError( - `Puppeteer error while rendering "${yellow(route.route)}"`, - err, - ' we will retry rendering this page up to 3 times.' - ); + logError(`Puppeteer error while rendering "${yellow(route.route)}"`, err, ' we will retry rendering this page up to 3 times.'); if (message && message.includes('closed')) { /** signal the launched to relaunch puppeteer, as it has likely died here. */ reLaunch('closed'); diff --git a/libs/scully/src/lib/routerPlugins/addOptionalRoutesPlugin.ts b/libs/scully/src/lib/routerPlugins/addOptionalRoutesPlugin.ts index 24201304a..4840d3f77 100644 --- a/libs/scully/src/lib/routerPlugins/addOptionalRoutesPlugin.ts +++ b/libs/scully/src/lib/routerPlugins/addOptionalRoutesPlugin.ts @@ -1,90 +1,33 @@ -import { Serializable } from 'puppeteer'; -import { findPlugin } from '../pluginManagement/pluginConfig'; +import { plugins } from '../pluginManagement/pluginRepository'; import { scullyConfig } from '../utils/config'; import { RoutesTypes, RouteTypes } from '../utils/interfacesandenums'; import { logError, logWarn, yellow } from '../utils/log'; +import { HandledRoute } from './handledRoute.interface'; -export const addOptionalRoutes = async ( - routeList = [] as string[] -): Promise => { - const routesToGenerate = await routeList.reduce( - async (result: Promise, cur: string) => { - const x = await result; - const config = scullyConfig.routes[cur]; - if (config) { - const postRenderers: (string | symbol)[] = Array.isArray( - config.postRenderers - ) - ? (config.postRenderers as (string | symbol)[]) - : undefined; - /** adding in the postrenderes. Note that the plugin might choose to overwrite the ones that come from the config */ - const r = (await routePluginHandler(cur)).map((row) => - postRenderers ? { postRenderers, ...row, config } : { ...row, config } - ); - x.push(...r); - } else if (cur.includes('/:')) { - logWarn(`No configuration for route "${yellow(cur)}" found. Skipping`); - } else { - x.push({ route: cur, type: RouteTypes.default }); - } - return x; - }, - Promise.resolve([] as HandledRoute[]) - ); +export const addOptionalRoutes = async (routeList = [] as string[]): Promise => { + const routesToGenerate = await routeList.reduce(async (result: Promise, cur: string) => { + const x = await result; + const config = scullyConfig.routes[cur]; + if (config) { + const postRenderers: (string | symbol)[] = Array.isArray(config.postRenderers) + ? (config.postRenderers as (string | symbol)[]) + : undefined; + /** adding in the postrenderes. Note that the plugin might choose to overwrite the ones that come from the config */ + const r = (await routePluginHandler(cur)).map( + (row) => (postRenderers ? { postRenderers, ...row, config } : { ...row, config }) as HandledRoute + ); + x.push(...r); + } else if (cur.includes('/:')) { + logWarn(`No configuration for route "${yellow(cur)}" found. Skipping`); + } else { + x.push({ route: cur, type: RouteTypes.default }); + } + return x; + }, Promise.resolve([] as HandledRoute[])); return routesToGenerate; }; -interface RouteConfig { - /** this route does a manual Idle check */ - manualIdleCheck?: boolean; - /** type of the route */ - type?: string; - /** - * an optional function that will be executed on render. - * Receives the route string, and the config of this route. - */ - preRenderer?: (route: HandledRoute) => Promise; - /** Allow in every other setting possible, depends on plugins */ - [key: string]: any; -} - -export interface HandledRoute { - /** the _complete_ route */ - route: string; - /** String, must be an existing plugin name. mandatory */ - type: string; - /** the relevant part of the scully-config */ - config?: RouteConfig; - /** variables exposed to angular _while rendering only!_ */ - exposeToPage?: { - manualIdle?: boolean; - transferState?: Serializable; - [key: string]: Serializable; - }; - /** data will be injected into the static page */ - injectToPage?: { - [key: string]: Serializable; - }; - /** an array with render plugin names that will be executed */ - postRenderers?: (string | symbol)[]; - /** the path to the file for a content file */ - templateFile?: string; - /** optional title, if data holds a title, that will be used instead */ - title?: string; - /** - * additional data that will end up in scully.routes.json - * the frontMatter data will be added here too. - */ - data?: RouteData; -} - -export interface RouteData { - title?: string; - author?: string; - [prop: string]: any; -} - async function routePluginHandler(route: string): Promise { const config = scullyConfig.routes; const conf = config[route] as RoutesTypes; @@ -92,18 +35,12 @@ async function routePluginHandler(route: string): Promise { logError(`No configuration for route "${yellow(route)}" found. Skipping`); return [{ route, type: RouteTypes.default }]; } - const routerPlugin = findPlugin(conf.type, 'router', false); - if (routerPlugin) { - const generatedRoutes = (await (routerPlugin( - route, - conf - ) as unknown)) as HandledRoute[]; + if (plugins.router[conf.type]) { + const generatedRoutes = (await (plugins.router[conf.type](route, conf) as unknown)) as HandledRoute[]; generatedRoutes.forEach((handledRoute) => { if (!handledRoute.route.startsWith('/')) { logWarn( - `The plugin '${ - conf.type - }' needs to return handledRoutes with a route that starts with '/'. The route ${JSON.stringify( + `The plugin '${conf.type}' needs to return handledRoutes with a route that starts with '/'. The route ${JSON.stringify( handledRoute )} is invalid.` ); diff --git a/libs/scully/src/lib/routerPlugins/contentFolderPlugin.ts b/libs/scully/src/lib/routerPlugins/contentFolderPlugin.ts index a05f19273..2bba0f5e1 100644 --- a/libs/scully/src/lib/routerPlugins/contentFolderPlugin.ts +++ b/libs/scully/src/lib/routerPlugins/contentFolderPlugin.ts @@ -1,33 +1,22 @@ import { lstatSync, readdir, readFileSync } from 'fs'; import { basename, extname, join } from 'path'; -import { - AlternateExtensionsForFilePlugin, - FilePlugin, - plugins, - registerPlugin, -} from '../pluginManagement/pluginRepository'; +import { AlternateExtensionsForFilePlugin, plugins, registerPlugin } from '../pluginManagement/pluginRepository'; +import { FilePlugin } from '../pluginManagement/Plugin.interfaces'; import { readFileAndCheckPrePublishSlug } from '../renderPlugins/content-render-utils/readFileAndCheckPrePublishSlug'; import { scullyConfig } from '../utils/config'; import { RouteTypeContentFolder } from '../utils/interfacesandenums'; import { log, logWarn, yellow } from '../utils/log'; -import { HandledRoute } from './addOptionalRoutesPlugin'; +import { HandledRoute } from './handledRoute.interface'; let basePath: string; -export async function contentFolderPlugin( - angularRoute: string, - conf: RouteTypeContentFolder -): Promise { +export async function contentFolderPlugin(angularRoute: string, conf: RouteTypeContentFolder): Promise { const parts = angularRoute.split('/'); /** for now, just handle the First parameter. Not sure if/how we can handle multiple ones. */ - const param = parts - .filter((p) => p.startsWith(':')) - .map((id) => id.slice(1))[0]; + const param = parts.filter((p) => p.startsWith(':')).map((id) => id.slice(1))[0]; const paramConfig = conf[param]; if (!paramConfig) { - console.error( - `missing config for parameters (${param}) in route: ${angularRoute}. Skipping` - ); + console.error(`missing config for parameters (${param}) in route: ${angularRoute}. Skipping`); return []; } const baseRoute = angularRoute.split(':' + param)[0]; @@ -37,9 +26,7 @@ export async function contentFolderPlugin( } async function checkSourceIsDirectoryAndRun(path, baseRoute, conf) { - const files = await new Promise((resolve) => - readdir(path, (err, data) => resolve(data)) - ); + const files = await new Promise((resolve) => readdir(path, (err, data) => resolve(data))); const handledRoutes: HandledRoute[] = []; for (const sourceFile of files) { const ext = extname(sourceFile); @@ -47,30 +34,16 @@ async function checkSourceIsDirectoryAndRun(path, baseRoute, conf) { const templateFile = join(path, sourceFile); if (lstatSync(templateFile).isDirectory()) { - handledRoutes.push( - ...(await checkSourceIsDirectoryAndRun(templateFile, baseRoute, conf)) - ); + handledRoutes.push(...(await checkSourceIsDirectoryAndRun(templateFile, baseRoute, conf))); } else { if (checkIfEmpty(templateFile)) { - logWarn( - `The file ${yellow(templateFile)} is empty, scully will ignore.` - ); + logWarn(`The file ${yellow(templateFile)} is empty, scully will ignore.`); } else if (!hasContentPlugin(ext)) { logWarn( - `The file ${yellow(templateFile)} has extension ${yellow( - ext - )} that has no plugin defined, scully will skip this file.` + `The file ${yellow(templateFile)} has extension ${yellow(ext)} that has no plugin defined, scully will skip this file.` ); } else { - handledRoutes.push( - ...(await addHandleRoutes( - sourceFile, - baseRoute, - templateFile, - conf, - ext - )) - ); + handledRoutes.push(...(await addHandleRoutes(sourceFile, baseRoute, templateFile, conf, ext))); } } } @@ -85,8 +58,7 @@ function hasContentPlugin(extension: string) { Object.entries(availAblePlugins).find( ([name, plugin]: [string, FilePlugin]) => extension === name.toLowerCase() || - (Array.isArray(plugin[AlternateExtensionsForFilePlugin]) && - plugin[AlternateExtensionsForFilePlugin].includes(extension)) + (Array.isArray(plugin[AlternateExtensionsForFilePlugin]) && plugin[AlternateExtensionsForFilePlugin].includes(extension)) ) !== undefined ); } @@ -109,14 +81,10 @@ async function addHandleRoutes(sourceFile, baseRoute, templateFile, conf, ext) { const newTemplateFile = templateFile.split('\\').join('/'); if (!newTemplateFile.endsWith(`${basePath}/${sourceFile}`)) { /** get the 'path' part of as a route partial */ - const routePartial = newTemplateFile - .substr(basePath.length + 1) - .replace(sourceFile, ''); + const routePartial = newTemplateFile.substr(basePath.length + 1).replace(sourceFile, ''); routify = (frag) => `${baseRoute}${routePartial}${slugify(frag)}`; } - const { meta, prePublished } = await readFileAndCheckPrePublishSlug( - templateFile - ); + const { meta, prePublished } = await readFileAndCheckPrePublishSlug(templateFile); const name = conf.name; const handledRoute: HandledRoute = { route: routify(meta.slug || base), @@ -136,9 +104,7 @@ async function addHandleRoutes(sourceFile, baseRoute, templateFile, conf, ext) { } export function slugify(frag: string): string { - return encodeURIComponent( - frag.trim().split('/').join('_').split(' ').join('_').split('?').join('_') - ); + return encodeURIComponent(frag.trim().split('/').join('_').split(' ').join('_').split('?').join('_')); } // TODO actual validation of the config diff --git a/libs/scully/src/lib/routerPlugins/handledRoute.interface.ts b/libs/scully/src/lib/routerPlugins/handledRoute.interface.ts new file mode 100644 index 000000000..96aaa71e4 --- /dev/null +++ b/libs/scully/src/lib/routerPlugins/handledRoute.interface.ts @@ -0,0 +1,50 @@ +import { Serializable } from 'puppeteer'; +interface RouteConfig { + /** this route does a manual Idle check */ + manualIdleCheck?: boolean; + /** type of the route */ + type?: string; + /** + * an optional function that will be executed on render. + * Receives the route string, and the config of this route. + */ + preRenderer?: (route: HandledRoute) => Promise; + /** Allow in every other setting possible, depends on plugins */ + [key: string]: any; +} + +export interface HandledRoute { + /** the _complete_ route */ + route: string; + /** String, must be an existing plugin name. mandatory */ + type: string; + /** the relevant part of the scully-config */ + config?: RouteConfig; + /** variables exposed to angular _while rendering only!_ */ + exposeToPage?: { + manualIdle?: boolean; + transferState?: Serializable; + [key: string]: Serializable; + }; + /** data will be injected into the static page */ + injectToPage?: { + [key: string]: Serializable; + }; + /** an array with render plugin names that will be executed */ + postRenderers?: string[]; + /** the path to the file for a content file */ + templateFile?: string; + /** optional title, if data holds a title, that will be used instead */ + title?: string; + /** + * additional data that will end up in scully.routes.json + * the frontMatter data will be added here too. + */ + data?: RouteData; +} + +export interface RouteData { + title?: string; + author?: string; + [prop: string]: any; +} diff --git a/libs/scully/src/lib/routerPlugins/index.ts b/libs/scully/src/lib/routerPlugins/index.ts index 036c0793d..6948531dc 100644 --- a/libs/scully/src/lib/routerPlugins/index.ts +++ b/libs/scully/src/lib/routerPlugins/index.ts @@ -5,3 +5,4 @@ export * from './ignoredRoutePlugin'; export * from './jsonRoutePlugin'; export * from './renderTemplate'; export * from './traverseAppRoutesPlugin'; +export * from './handledRoute.interface'; diff --git a/libs/scully/src/lib/routerPlugins/jsonRoutePlugin.ts b/libs/scully/src/lib/routerPlugins/jsonRoutePlugin.ts index c3814284a..f1a1210e9 100644 --- a/libs/scully/src/lib/routerPlugins/jsonRoutePlugin.ts +++ b/libs/scully/src/lib/routerPlugins/jsonRoutePlugin.ts @@ -1,31 +1,19 @@ -import { - configValidator, - registerPlugin -} from '../pluginManagement/pluginRepository'; +import { configValidator, registerPlugin } from '../pluginManagement/pluginRepository'; import { httpGetJson } from '../utils/httpGetJson'; import { RouteTypeJson } from '../utils/interfacesandenums'; import { logError, yellow } from '../utils/log'; import { routeSplit } from '../utils/routeSplit'; -import { HandledRoute } from './addOptionalRoutesPlugin'; +import { HandledRoute } from './handledRoute.interface'; import { deepGet } from '../utils/deepGet'; import { renderTemplate } from './renderTemplate'; -export const jsonRoutePlugin = async ( - route: string, - conf: RouteTypeJson -): Promise => { +export const jsonRoutePlugin = async (route: string, conf: RouteTypeJson): Promise => { try { const { params, createPath } = routeSplit(route); // const params = parts.filter(p => p.startsWith(':')).map(id => id.slice(1)); - const missingParams = params.filter( - param => !conf.hasOwnProperty(param.part) - ); + const missingParams = params.filter((param) => !conf.hasOwnProperty(param.part)); if (missingParams.length > 0) { - console.error( - `missing config for parameters (${missingParams.join( - ',' - )}) in route: ${route}. Skipping` - ); + console.error(`missing config for parameters (${missingParams.join(',')}) in route: ${route}. Skipping`); return [{ route, type: conf.type }]; } @@ -34,17 +22,11 @@ export const jsonRoutePlugin = async ( /** us es-template lie string to construct the url */ const url = renderTemplate(conf[param.part].url, context).trim(); return httpGetJson(url, { - headers: conf[param.part].headers + headers: conf[param.part].headers, }) - .then(rawData => - conf[param.part].resultsHandler - ? conf[param.part].resultsHandler(rawData) - : rawData - ) + .then((rawData) => (conf[param.part].resultsHandler ? conf[param.part].resultsHandler(rawData) : rawData)) .then((rawData: any) => - conf[param.part].property === undefined - ? rawData - : rawData.map(row => deepGet(conf[param.part].property, row)) + conf[param.part].property === undefined ? rawData : rawData.map((row) => deepGet(conf[param.part].property, row)) ); }; @@ -55,22 +37,22 @@ export const jsonRoutePlugin = async ( * first iteration, just dump the top level in * and convert it to array format. */ - return (await loadData(param)).map(r => [r]); + return (await loadData(param)).map((r) => [r]); } return await Promise.all( - foundRoutes.map(async data => { + foundRoutes.map(async (data) => { const context = data.reduce((ctx, r, x) => { return { ...ctx, [params[x].part]: r }; }, {}); const additionalRoutes = await loadData(param, context); - return additionalRoutes.map(r => [...data, r]); + return additionalRoutes.map((r) => [...data, r]); }, []) - ).then(chunks => chunks.reduce((acc, cur) => acc.concat(cur))); + ).then((chunks) => chunks.reduce((acc, cur) => acc.concat(cur))); }, Promise.resolve([])); return routes.map((routeData: string[]) => ({ route: createPath(...routeData), - type: conf.type + type: conf.type, })); } catch (e) { logError(`Could not fetch data for route "${yellow(route)}"`); @@ -79,7 +61,7 @@ export const jsonRoutePlugin = async ( }; // TODO actual validation of the config -const jsonValidator = async conf => { +const jsonValidator = async (conf) => { const { params } = routeSplit(conf.path); // return [yellow('all seems ok')]; return []; diff --git a/libs/scully/src/lib/systemPlugins/storeRoutes.ts b/libs/scully/src/lib/systemPlugins/storeRoutes.ts index 2a7393a48..1255513cd 100644 --- a/libs/scully/src/lib/systemPlugins/storeRoutes.ts +++ b/libs/scully/src/lib/systemPlugins/storeRoutes.ts @@ -1,6 +1,6 @@ import { writeFileSync } from 'fs'; import { join } from 'path'; -import { HandledRoute } from '../routerPlugins/addOptionalRoutesPlugin'; +import { HandledRoute } from '../routerPlugins/handledRoute.interface'; import { watch } from '../utils/cli-options'; import { scullyConfig } from '../utils/config'; import { createFolderFor } from '../utils/createFolderFor'; @@ -25,9 +25,7 @@ async function storeRoutesPlugin(routes: HandledRoute[]) { join(scullyConfig.homeFolder, scullyConfig.sourceRoot, routesFileName) ); } else { - logWarn( - `running in watch-mode, routefile in source assets will not be updated` - ); + logWarn(`running in watch-mode, routefile in source assets will not be updated`); } try { const jsonResult = JSON.stringify( diff --git a/libs/scully/src/lib/systemPlugins/writeToFs.plugin.ts b/libs/scully/src/lib/systemPlugins/writeToFs.plugin.ts index 022897fbe..f8fbb6956 100644 --- a/libs/scully/src/lib/systemPlugins/writeToFs.plugin.ts +++ b/libs/scully/src/lib/systemPlugins/writeToFs.plugin.ts @@ -5,6 +5,7 @@ import { findPlugin } from '../pluginManagement/pluginConfig'; import { scullyConfig } from '../utils/config'; import { createFolderFor } from '../utils/createFolderFor'; import { log, logError, yellow } from '../utils/log'; +import { accessPluginDirectly } from '../pluginManagement/pluginRepository'; const { writeFile } = promises; const SCULLY_STATE_START = `/** ___SCULLY_STATE_START___ */`; @@ -28,17 +29,12 @@ const writeHTMLToFs = async (route: string, content: string): Promise => { /** plugin that saves State (if there) to data.json */ const writeDataToFs = async (route: string, content: string): Promise => { - const state: string = findPlugin(ExtractState)(route, content); + const state: string = findPlugin(ExtractState)[accessPluginDirectly](route, content); if (!scullyConfig.inlineStateOnly && state) { const stateFile = join(scullyConfig.outDir, route, '/data.json'); await writeFile(stateFile, state); const dataSize = Math.floor((state.length / 1024) * 100) / 100; - log( - `${` ${dataSize}Kb`.padStart( - 12 + route.length, - ' ' - )} data into file: "${yellow(stateFile)}"` - ); + log(`${` ${dataSize}Kb`.padStart(12 + route.length, ' ')} data into file: "${yellow(stateFile)}"`); //TODO: add warning for data size? } }; diff --git a/libs/scully/src/lib/utils/deepClone.ts b/libs/scully/src/lib/utils/deepClone.ts index 325b801b6..8d6aac076 100644 --- a/libs/scully/src/lib/utils/deepClone.ts +++ b/libs/scully/src/lib/utils/deepClone.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-prototype-builtins */ import { Serializable } from 'puppeteer'; /** * Helper to deep-clone any JS variable. diff --git a/libs/scully/src/lib/utils/handlers/defaultAction.ts b/libs/scully/src/lib/utils/handlers/defaultAction.ts index 68d10efad..b2c83e5b2 100644 --- a/libs/scully/src/lib/utils/handlers/defaultAction.ts +++ b/libs/scully/src/lib/utils/handlers/defaultAction.ts @@ -1,16 +1,13 @@ -import { - findPlugin, - registerPlugin, - scullySystem, -} from '../../pluginManagement'; +import { findPlugin, registerPlugin, scullySystem } from '../../pluginManagement'; import { launchedBrowser } from '../../renderPlugins/launchedBrowser'; -import { HandledRoute } from '../../routerPlugins/addOptionalRoutesPlugin'; +import { HandledRoute } from '../../routerPlugins/handledRoute.interface'; import { baseFilter } from '../cli-options'; import { loadConfig } from '../config'; import { log } from '../log'; import { handleAllDone } from './handleAllDone'; import { handleRouteDiscoveryDone } from './handleRouteDiscoveryDone'; import { handleTravesal } from './handleTravesal'; +import { processRoutes } from './processRoutes'; import { renderParallel } from './renderParallel'; import { routeDiscovery } from './routeDiscovery'; @@ -22,12 +19,12 @@ async function plugin(localBaseFilter = baseFilter): Promise { try { const unhandledRoutes = await findPlugin(handleTravesal)(); - const handledRoutes = await routeDiscovery( - unhandledRoutes, - localBaseFilter - ); + const handledRoutes = await routeDiscovery(unhandledRoutes, localBaseFilter); - const discoveryDone = handleRouteDiscoveryDone(handledRoutes); + /** handle routeProcess plugins */ + const processedRoutes = await findPlugin(processRoutes)(handledRoutes); + + const discoveryDone = handleRouteDiscoveryDone(processedRoutes); /** launch the browser, its shared among renderers */ await launchedBrowser(); diff --git a/libs/scully/src/lib/utils/handlers/handleAllDone.ts b/libs/scully/src/lib/utils/handlers/handleAllDone.ts index c583609d9..c265db033 100644 --- a/libs/scully/src/lib/utils/handlers/handleAllDone.ts +++ b/libs/scully/src/lib/utils/handlers/handleAllDone.ts @@ -1,6 +1,6 @@ import { performance } from 'perf_hooks'; import { plugins } from '../../pluginManagement/pluginRepository'; -import { HandledRoute } from '../../routerPlugins/addOptionalRoutesPlugin'; +import { HandledRoute } from '../../routerPlugins/handledRoute.interface'; import { deepClone } from '../deepClone'; import { performanceIds } from '../performanceIds'; @@ -9,8 +9,6 @@ export async function handleAllDone(handledRoutes: HandledRoute[]) { performance.mark('startAllDonePlugins'); performanceIds.add('AllDonePlugins'); const clone = deepClone(handledRoutes); - await Promise.all( - Object.values(plugins.allDone).map(plugin => plugin(clone)) - ); + await Promise.all(Object.values(plugins.allDone).map((plugin) => plugin(clone))); performance.mark('stopAllDonePlugins'); } diff --git a/libs/scully/src/lib/utils/handlers/handleRouteDiscoveryDone.ts b/libs/scully/src/lib/utils/handlers/handleRouteDiscoveryDone.ts index a2322ce25..5e49561b9 100644 --- a/libs/scully/src/lib/utils/handlers/handleRouteDiscoveryDone.ts +++ b/libs/scully/src/lib/utils/handlers/handleRouteDiscoveryDone.ts @@ -1,6 +1,6 @@ import { performance } from 'perf_hooks'; import { plugins } from '../../pluginManagement/pluginRepository'; -import { HandledRoute } from '../../routerPlugins/addOptionalRoutesPlugin'; +import { HandledRoute } from '../../routerPlugins/handledRoute.interface'; import { deepClone } from '../deepClone'; import { performanceIds } from '../performanceIds'; @@ -9,8 +9,6 @@ export async function handleRouteDiscoveryDone(handledRoutes: HandledRoute[]) { performance.mark('startRouteDonePlugins'); performanceIds.add('RouteDonePlugins'); const clone = deepClone(handledRoutes); - await Promise.all( - Object.values(plugins.routeDiscoveryDone).map(plugin => plugin(clone)) - ); + await Promise.all(Object.values(plugins.routeDiscoveryDone).map((plugin) => plugin(clone))); performance.mark('stopRouteDonePlugins'); } diff --git a/libs/scully/src/lib/utils/handlers/processRoutes.ts b/libs/scully/src/lib/utils/handlers/processRoutes.ts new file mode 100644 index 000000000..5b444689b --- /dev/null +++ b/libs/scully/src/lib/utils/handlers/processRoutes.ts @@ -0,0 +1,43 @@ +import { performance } from 'perf_hooks'; +import { + accessPluginDirectly, + findPlugin, + plugins, + registerPlugin, + RouteProcess, + routeProcessPriority, + scullySystem, +} from '../../pluginManagement'; +import { HandledRoute } from '../../routerPlugins/handledRoute.interface'; +import { storeRoutes } from '../../systemPlugins/storeRoutes'; +import { routeFilter, baseFilter } from '../cli-options'; +import { logError } from '../log'; +import { performanceIds } from '../performanceIds'; + +export const processRoutes = Symbol('processRoutes'); + +async function processRoutesPlugin(routes: HandledRoute[]): Promise { + performance.mark('startRouteProcess'); + performanceIds.add('RouteProcess'); + let result: HandledRoute[]; + try { + result = await Object.values(plugins.routeProcess) + .sort((a, b) => (a[accessPluginDirectly][routeProcessPriority] < b[accessPluginDirectly][routeProcessPriority] ? -1 : 1)) + .reduce(async (previousRoutes: Promise, routeProcessor: RouteProcess) => { + return await routeProcessor(await previousRoutes); + }, Promise.resolve(routes)); + } catch (e) { + logError(`Problem during route processing, see below for details. Skipped changing routes`); + console.error(e); + result = routes; + } + /** save routerinfo, so its available during rendering */ + if (baseFilter === '' && routeFilter === '') { + /** only store when the routes are complete */ + await findPlugin(storeRoutes)(result); + } + performance.mark('stopRouteProcess'); + return result; +} + +registerPlugin(scullySystem, processRoutes, processRoutesPlugin); diff --git a/libs/scully/src/lib/utils/handlers/routeDiscovery.ts b/libs/scully/src/lib/utils/handlers/routeDiscovery.ts index dce8896f4..1810c2166 100644 --- a/libs/scully/src/lib/utils/handlers/routeDiscovery.ts +++ b/libs/scully/src/lib/utils/handlers/routeDiscovery.ts @@ -1,18 +1,11 @@ import { performance } from 'perf_hooks'; -import { - addOptionalRoutes, - HandledRoute, -} from '../../routerPlugins/addOptionalRoutesPlugin'; -import { storeRoutes } from '../../systemPlugins/storeRoutes'; +import { addOptionalRoutes } from '../../routerPlugins/addOptionalRoutesPlugin'; +import { HandledRoute } from '../../routerPlugins/handledRoute.interface'; +import { routeFilter } from '../cli-options'; import { log, logError } from '../log'; import { performanceIds } from '../performanceIds'; -import { routeFilter } from '../cli-options'; -import { findPlugin } from '../../pluginManagement'; -export async function routeDiscovery( - unhandledRoutes: string[], - localBaseFilter: string -): Promise { +export async function routeDiscovery(unhandledRoutes: string[], localBaseFilter: string): Promise { performance.mark('startDiscovery'); performanceIds.add('Discovery'); log('Pull in data to create additional routes.'); @@ -26,36 +19,24 @@ export async function routeDiscovery( handledRoutes = ( await addOptionalRoutes( /** use all handled routes without empty ones, and apply the baseFilter */ - unhandledRoutes.filter( - (r: string) => - typeof r === 'string' && - baseFilterRegexs.some((reg) => r.match(reg) !== null) - ) + unhandledRoutes.filter((r: string) => typeof r === 'string' && baseFilterRegexs.some((reg) => r.match(reg) !== null)) ) ).filter( (r) => !r.route.endsWith('*') && /** use the routefilter to only include matches */ - (routeFilter === '' || - routeFilterRegexs.some((reg) => r.route.match(reg) !== null)) + (routeFilter === '' || routeFilterRegexs.some((reg) => r.route.match(reg) !== null)) ); } catch (e) { logError(`Problem during route handling, see below for details`); console.error(e); } performance.mark('stopDiscovery'); - /** save routerinfo, so its available during rendering */ - if (localBaseFilter === '' && routeFilter === '') { - /** only store when the routes are complete */ - await findPlugin(storeRoutes)(handledRoutes); - } + return handledRoutes; } -function wildCardStringToRegEx( - string, - { addTrailingStar } = { addTrailingStar: false } -) { +function wildCardStringToRegEx(string, { addTrailingStar } = { addTrailingStar: false }) { const t = string.split(','); return t.map((item) => { if (addTrailingStar) { diff --git a/libs/scully/src/lib/utils/interfacesandenums.ts b/libs/scully/src/lib/utils/interfacesandenums.ts index 084db8135..dc5a96d85 100644 --- a/libs/scully/src/lib/utils/interfacesandenums.ts +++ b/libs/scully/src/lib/utils/interfacesandenums.ts @@ -62,11 +62,7 @@ interface RouteConfig { [route: string]: RoutesTypes; } -export type RoutesTypes = - | RouteTypeJson - | RouteTypeContentFolder - | RouterTypeDefault - | RouteTypeUnknown; +export type RoutesTypes = RouteTypeJson | RouteTypeContentFolder | RouterTypeDefault | RouteTypeUnknown; export interface RouterTypeDefault { type: RouteTypes.default; diff --git a/libs/scully/src/lib/utils/serverstuff/handleUnknownRoute.ts b/libs/scully/src/lib/utils/serverstuff/handleUnknownRoute.ts index a411805b0..e74fab850 100644 --- a/libs/scully/src/lib/utils/serverstuff/handleUnknownRoute.ts +++ b/libs/scully/src/lib/utils/serverstuff/handleUnknownRoute.ts @@ -10,26 +10,16 @@ import { logError, logWarn, yellow } from '../log'; import { pathToRegexp } from 'path-to-regexp'; import { title404 } from './title404'; import { loadConfig } from '../config'; -import { HandledRoute } from '../../routerPlugins'; +import { HandledRoute } from '../../routerPlugins/'; export const handleUnknownRoute: RequestHandler = async (req, res, next) => { if (req.accepts('html')) { /** only handle 404 on html requests specially */ await loadConfig; - const distIndex = join( - scullyConfig.homeFolder, - scullyConfig.distFolder, - '/index.html' - ); - const dist404 = join( - scullyConfig.homeFolder, - scullyConfig.distFolder, - '/404.html' - ); + const distIndex = join(scullyConfig.homeFolder, scullyConfig.distFolder, '/index.html'); + const dist404 = join(scullyConfig.homeFolder, scullyConfig.distFolder, '/404.html'); // cmd-line takes precedence over config - const h404 = (handle404.trim() === '' ? scullyConfig.handle404 : handle404) - .trim() - .toLowerCase(); + const h404 = (handle404.trim() === '' ? scullyConfig.handle404 : handle404).trim().toLowerCase(); switch (h404) { case '': @@ -77,9 +67,7 @@ export const handleUnknownRoute: RequestHandler = async (req, res, next) => { next(); }; -function matchRoute( - req -): (value: string, index: number, obj: string[]) => boolean { +function matchRoute(req): (value: string, index: number, obj: string[]) => boolean { return (route) => { try { const path = req.url; @@ -108,9 +96,7 @@ function loadHandledRoutes(): string[] { const tdLastModified = statSync(path).mtimeMs; if (lastTime < tdLastModified) { try { - const routes = JSON.parse( - readFileSync(path, 'utf-8').toString() - ) as HandledRoute[]; + const routes = JSON.parse(readFileSync(path, 'utf-8').toString()) as HandledRoute[]; handledRoutes.clear(); routes.forEach((r) => handledRoutes.add(r.route)); lastTime = tdLastModified; diff --git a/libs/scully/src/lib/utils/validateConfig.ts b/libs/scully/src/lib/utils/validateConfig.ts index a87afd6c5..d1f582196 100644 --- a/libs/scully/src/lib/utils/validateConfig.ts +++ b/libs/scully/src/lib/utils/validateConfig.ts @@ -1,6 +1,7 @@ +/* eslint-disable no-prototype-builtins */ import { existsSync } from 'fs'; import { join } from 'path'; -import { configValidator, plugins } from '../pluginManagement/pluginRepository'; +import { configValidator, plugins, accessPluginDirectly } from '../pluginManagement/pluginRepository'; import { angularRoot } from './config'; import { ScullyConfig } from './interfacesandenums'; import { logError, logWarn, yellow } from './log'; @@ -14,7 +15,7 @@ const error = (...args) => { logError(...args); }; -export async function validateConfig(config: ScullyConfig) { +export async function validateConfig(config: ScullyConfig): Promise { // log(`Checking "${yellow('scully.json')}"`); /** make sure the config is completely loaded */ // await loadConfig; @@ -26,23 +27,13 @@ export async function validateConfig(config: ScullyConfig) { error(`Type missing in route "${yellow(route)}"`); } if (!plugins.router.hasOwnProperty(definition.type)) { - error( - `Unknown type "${yellow(definition.type)}" in route "${yellow( - route - )}"` - ); + error(`Unknown type "${yellow(definition.type)}" in route "${yellow(route)}"`); } else { + const curPlugin = plugins.router[definition.type][accessPluginDirectly]; const pluginErrors: string[] = - (plugins.router[definition.type] && - plugins.router[definition.type][configValidator] && - (await plugins.router[definition.type][configValidator]( - definition - ))) || - []; + (curPlugin && curPlugin[configValidator] && (await curPlugin[configValidator](definition))) || []; if (pluginErrors.length) { - error(`Route ${yellow( - route - )} has the following configuration issue(s): ${pluginErrors.map( + error(`Route ${yellow(route)} has the following configuration issue(s): ${pluginErrors.map( (errMsg, i) => `\n ${i + 1} ${errMsg}` )} `); diff --git a/libs/scully/tsconfig.json b/libs/scully/tsconfig.json index 12ac88cd4..682697012 100644 --- a/libs/scully/tsconfig.json +++ b/libs/scully/tsconfig.json @@ -18,6 +18,6 @@ "typeRoots": [], "allowSyntheticDefaultImports": true }, - "files": ["src/index.ts", "src/scully.ts"], + "files": ["src/index.ts", "src/scully.ts", "./src/lib/utils/handlers/processRoutes.ts"], "exclude": ["../../dist/**/*"] } diff --git a/scully.sample-blog.config.ts b/scully.sample-blog.config.ts index cc52436f6..4e6e15105 100644 --- a/scully.sample-blog.config.ts +++ b/scully.sample-blog.config.ts @@ -2,12 +2,7 @@ import '@scullyio/from-data'; // import './demos/plugins/extra-plugin.js'; import '@scullyio/plugin-extra'; -import { - HandledRoute, - registerPlugin, - ScullyConfig, - setPluginConfig, -} from '@scullyio/scully'; +import { HandledRoute, registerPlugin, ScullyConfig, setPluginConfig, logError } from '@scullyio/scully'; import { baseHrefRewrite } from '@scullyio/scully-plugin-base-href-rewrite'; import { getFlashPreventionPlugin } from '@scullyio/scully-plugin-flash-prevention'; import './demos/plugins/errorPlugin'; @@ -130,9 +125,7 @@ export const config: ScullyConfig = { }, }, guessParserOptions: { - excludedFiles: [ - 'apps/sample-blog/src/app/exclude/exclude-routing.module.ts', - ], + excludedFiles: ['apps/sample-blog/src/app/exclude/exclude-routing.module.ts'], }, }; @@ -144,6 +137,28 @@ const fakeroutePlugin = async (): Promise => [ registerPlugin('router', 'addFake', fakeroutePlugin); +registerPlugin( + 'routeProcess', + 'test2', + (r: HandledRoute[]) => + r.map((route) => { + const { data } = route; + const { nonsense, ...rest } = data; + if (nonsense !== 'do remove this please!') { + logError('things are wrong, test failed on processRoutes test2 (sample-blog.config)'); + process.exit(15); + } + return { ...route, data: { ...rest } }; + }), + 30 +); +registerPlugin( + 'routeProcess', + 'test1', + (r: HandledRoute[]) => r.map((line) => ({ ...line, data: { ...line.data, nonsense: 'do remove this please!' } })), + 20 +); + async function getMyRoutes(): Promise { return new Promise((r) => { console.log('this line should be visible for 15 seconds');