From 9791cdd74fccf10f7b1a6fd1a7b384a94c70181b Mon Sep 17 00:00:00 2001 From: Jovyn Tan Date: Mon, 13 Mar 2023 21:03:20 +0800 Subject: [PATCH 1/2] Rename Site/index.js to Typescript --- .eslintignore | 1 - .gitignore | 1 - packages/core/src/Site/{index.js => index.ts} | 0 3 files changed, 2 deletions(-) rename packages/core/src/Site/{index.js => index.ts} (100%) diff --git a/.eslintignore b/.eslintignore index f43ba514ec..673e8ddf68 100644 --- a/.eslintignore +++ b/.eslintignore @@ -26,7 +26,6 @@ packages/core/src/variables/*.js packages/core/test/unit/**/*.js # TODO: remove when migrated to TS -!packages/core/src/Site/index.js !packages/core/test/unit/lib/nunjucks-extensions/*.js !markdown-it-icons.test.js !Site.test.js diff --git a/.gitignore b/.gitignore index df66e1fa97..d69ad5bd20 100644 --- a/.gitignore +++ b/.gitignore @@ -91,7 +91,6 @@ packages/core/src/variables/*.js packages/core/test/unit/**/*.js # TODO: remove when migrated to TS -!packages/core/src/Site/index.js !packages/core/test/unit/lib/nunjucks-extensions/*.js !markdown-it-icons.test.js !Site.test.js diff --git a/packages/core/src/Site/index.js b/packages/core/src/Site/index.ts similarity index 100% rename from packages/core/src/Site/index.js rename to packages/core/src/Site/index.ts From 614999e67faa1963f009d2e45ed6cfeb66fe7e5a Mon Sep 17 00:00:00 2001 From: Jovyn Tan Date: Sat, 8 Apr 2023 10:16:45 +0800 Subject: [PATCH 2/2] Adapt Site/index.js to Typescript --- package-lock.json | 14 + packages/cli/src/cmd/build.js | 2 +- packages/cli/src/cmd/deploy.js | 2 +- packages/cli/src/cmd/init.js | 3 +- packages/cli/src/cmd/serve.js | 2 +- packages/core/package.json | 1 + packages/core/src/Site/SiteConfig.ts | 33 +- packages/core/src/Site/index.ts | 455 +++++++++++------- .../core/src/variables/VariableProcessor.ts | 7 +- packages/core/test/unit/Site.test.js | 2 +- 10 files changed, 319 insertions(+), 202 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1b650a1aba..eb1b48535e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4801,6 +4801,12 @@ "@types/node": "*" } }, + "node_modules/@types/gh-pages": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/gh-pages/-/gh-pages-3.2.1.tgz", + "integrity": "sha512-y5ULkwfoOEUa6sp2te+iEODv2S//DRiKmxpeXboXhhv+s758rSSxLUiBd6NnlR7aAY4nw1X4FGovLrSWEXWLow==", + "dev": true + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -24682,6 +24688,7 @@ "@types/crypto-js": "^4.1.1", "@types/domhandler": "^2.4.2", "@types/fs-extra": "^9.0.13", + "@types/gh-pages": "^3.2.1", "@types/htmlparser2": "^3.10.3", "@types/jest": "^27.4.1", "@types/js-beautify": "^1.13.3", @@ -27240,6 +27247,7 @@ "@types/crypto-js": "^4.1.1", "@types/domhandler": "^2.4.2", "@types/fs-extra": "^9.0.13", + "@types/gh-pages": "*", "@types/htmlparser2": "^3.10.3", "@types/jest": "^27.4.1", "@types/js-beautify": "^1.13.3", @@ -28801,6 +28809,12 @@ "@types/node": "*" } }, + "@types/gh-pages": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/gh-pages/-/gh-pages-3.2.1.tgz", + "integrity": "sha512-y5ULkwfoOEUa6sp2te+iEODv2S//DRiKmxpeXboXhhv+s758rSSxLUiBd6NnlR7aAY4nw1X4FGovLrSWEXWLow==", + "dev": true + }, "@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", diff --git a/packages/cli/src/cmd/build.js b/packages/cli/src/cmd/build.js index 3f6f54bde9..6c73ac863d 100755 --- a/packages/cli/src/cmd/build.js +++ b/packages/cli/src/cmd/build.js @@ -1,6 +1,6 @@ const path = require('path'); -const { Site } = require('@markbind/core'); +const { Site } = require('@markbind/core').Site; const cliUtil = require('../util/cliUtil'); const logger = require('../util/logger'); diff --git a/packages/cli/src/cmd/deploy.js b/packages/cli/src/cmd/deploy.js index f38c79ade9..475fed36cf 100755 --- a/packages/cli/src/cmd/deploy.js +++ b/packages/cli/src/cmd/deploy.js @@ -1,6 +1,6 @@ const path = require('path'); -const { Site } = require('@markbind/core'); +const { Site } = require('@markbind/core').Site; const cliUtil = require('../util/cliUtil'); const logger = require('../util/logger'); diff --git a/packages/cli/src/cmd/init.js b/packages/cli/src/cmd/init.js index 11e8390fb0..068f62ee69 100755 --- a/packages/cli/src/cmd/init.js +++ b/packages/cli/src/cmd/init.js @@ -1,7 +1,8 @@ const fs = require('fs-extra'); const path = require('path'); -const { Site, Template } = require('@markbind/core'); +const { Template } = require('@markbind/core'); +const { Site } = require('@markbind/core').Site; const logger = require('../util/logger'); diff --git a/packages/cli/src/cmd/serve.js b/packages/cli/src/cmd/serve.js index 7243730327..cd85c89773 100755 --- a/packages/cli/src/cmd/serve.js +++ b/packages/cli/src/cmd/serve.js @@ -1,7 +1,7 @@ const chokidar = require('chokidar'); const path = require('path'); -const { Site } = require('@markbind/core'); +const { Site } = require('@markbind/core').Site; const { pageVueServerRenderer } = require('@markbind/core/src/Page/PageVueServerRenderer'); const fsUtil = require('@markbind/core/src/utils/fsUtil'); diff --git a/packages/core/package.json b/packages/core/package.json index 47b76a81f0..2df92708dd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -80,6 +80,7 @@ "@types/crypto-js": "^4.1.1", "@types/domhandler": "^2.4.2", "@types/fs-extra": "^9.0.13", + "@types/gh-pages": "^3.2.1", "@types/htmlparser2": "^3.10.3", "@types/jest": "^27.4.1", "@types/js-beautify": "^1.13.3", diff --git a/packages/core/src/Site/SiteConfig.ts b/packages/core/src/Site/SiteConfig.ts index 438cb43a30..6815adb2ac 100644 --- a/packages/core/src/Site/SiteConfig.ts +++ b/packages/core/src/Site/SiteConfig.ts @@ -1,5 +1,24 @@ +import { FrontMatter } from '../plugins/Plugin'; + const HEADING_INDEXING_LEVEL_DEFAULT = 3; +export type SiteConfigPage = { + glob?: string, + layout?: string, + src?: string[], + title?: string, + externalScripts?: string[], + globExclude?: string, + searchable?: string | boolean, + frontmatter?: FrontMatter, +}; + +export type SiteConfigStyle = { + bootstrapTheme?: string; + codeTheme: 'dark' | 'light'; + codeLineNumbers: boolean; // Default hide display of line numbers for code blocks +}; + /** * Represents a read only site config read from the site configuration file, * with default values for unspecified properties. @@ -10,16 +29,9 @@ export class SiteConfig { faviconPath?: string; headingIndexingLevel: number; - style: { - bootstrapTheme?: string; - codeTheme: string; - /** - * Default hide display of line numbers for code blocks - */ - codeLineNumbers: boolean; - }; + style: SiteConfigStyle; - pages: string[]; + pages: SiteConfigPage[]; pagesExclude: string[]; ignore: string[]; @@ -40,6 +52,7 @@ export class SiteConfig { message?: string; repo?: string; branch?: string; + baseDir?: string; }; intrasiteLinkValidation: { @@ -87,5 +100,3 @@ export class SiteConfig { ? siteConfigJson.plantumlCheck : true; // check PlantUML's prerequisite by default } } - -module.exports = SiteConfig; diff --git a/packages/core/src/Site/index.ts b/packages/core/src/Site/index.ts index 6d4afd63e4..4c8fa08398 100644 --- a/packages/core/src/Site/index.ts +++ b/packages/core/src/Site/index.ts @@ -1,47 +1,65 @@ -const cheerio = require('cheerio'); require('../patches/htmlparser2'); -const fs = require('fs-extra'); -const ghpages = require('gh-pages'); -const ignore = require('ignore'); -const path = require('path'); -const Promise = require('bluebird'); -const walkSync = require('walk-sync'); -const simpleGit = require('simple-git'); +import cheerio from 'cheerio'; +import fs from 'fs-extra'; +import ignore, { Ignore } from 'ignore'; +import path from 'path'; +import walkSync from 'walk-sync'; +import simpleGit, { SimpleGit } from 'simple-git'; +import Bluebird from 'bluebird'; +import ghpages from 'gh-pages'; +import difference from 'lodash/difference'; +import differenceWith from 'lodash/differenceWith'; +import flatMap from 'lodash/flatMap'; +import has from 'lodash/has'; +import isBoolean from 'lodash/isBoolean'; +import isEmpty from 'lodash/isEmpty'; +import isEqual from 'lodash/isEqual'; +import isUndefined from 'lodash/isUndefined'; +import noop from 'lodash/noop'; +import omitBy from 'lodash/omitBy'; +import startCase from 'lodash/startCase'; +import union from 'lodash/union'; +import uniq from 'lodash/uniq'; + +import { Template as NunjucksTemplate } from 'nunjucks'; +import { SiteConfig, SiteConfigPage, SiteConfigStyle } from './SiteConfig'; +import { Page } from '../Page'; +import { PageConfig } from '../Page/PageConfig'; +import { VariableProcessor } from '../variables/VariableProcessor'; +import { VariableRenderer } from '../variables/VariableRenderer'; +import { ExternalManager, ExternalManagerConfig } from '../External/ExternalManager'; +import { SiteLinkManager } from '../html/SiteLinkManager'; +import { PluginManager } from '../plugins/PluginManager'; +import type { FrontMatter } from '../plugins/Plugin'; +import { sequentialAsyncForEach } from '../utils/async'; +import { delay } from '../utils/delay'; +import * as fsUtil from '../utils/fsUtil'; +import * as gitUtil from '../utils/git'; +import * as logger from '../utils/logger'; +import { SITE_CONFIG_NAME, INDEX_MARKDOWN_FILE, LAZY_LOADING_SITE_FILE_NAME } from './constants'; + +require('../patches/htmlparser2'); const ProgressBar = require('../lib/progress'); +const { LAYOUT_FOLDER_PATH, LAYOUT_DEFAULT_NAME, LayoutManager } = require('../Layout'); + +const _ = { + difference, + differenceWith, + flatMap, + has, + isUndefined, + isEqual, + isEmpty, + isBoolean, + noop, + omitBy, + startCase, + union, + uniq, +}; -const SiteConfig = require('./SiteConfig'); -const { Page } = require('../Page'); -const { PageConfig } = require('../Page/PageConfig'); -const { VariableProcessor } = require('../variables/VariableProcessor'); -const { VariableRenderer } = require('../variables/VariableRenderer'); -const { ExternalManager } = require('../External/ExternalManager'); -const { LayoutManager, LAYOUT_DEFAULT_NAME, LAYOUT_FOLDER_PATH } = require('../Layout'); -const { SiteLinkManager } = require('../html/SiteLinkManager'); -const { PluginManager } = require('../plugins/PluginManager'); - -const { sequentialAsyncForEach } = require('../utils/async'); -const { delay } = require('../utils/delay'); -const fsUtil = require('../utils/fsUtil'); -const gitUtil = require('../utils/git'); -const logger = require('../utils/logger'); -const { SITE_CONFIG_NAME, INDEX_MARKDOWN_FILE, LAZY_LOADING_SITE_FILE_NAME } = require('./constants'); - -const _ = {}; -_.difference = require('lodash/difference'); -_.differenceWith = require('lodash/differenceWith'); -_.flatMap = require('lodash/flatMap'); -_.has = require('lodash/has'); -_.isBoolean = require('lodash/isBoolean'); -_.isEmpty = require('lodash/isEmpty'); -_.isEqual = require('lodash/isEqual'); -_.isUndefined = require('lodash/isUndefined'); -_.noop = require('lodash/noop'); -_.omitBy = require('lodash/omitBy'); -_.startCase = require('lodash/startCase'); -_.union = require('lodash/union'); -_.uniq = require('lodash/uniq'); - -const url = {}; -url.join = path.posix.join; +const url = { + join: path.posix.join, +}; const MARKBIND_VERSION = require('../../package.json').version; @@ -66,11 +84,11 @@ const MAX_CONCURRENT_PAGE_GENERATION_PROMISES = 4; const LAZY_LOADING_BUILD_TIME_RECOMMENDATION_LIMIT = 30000; const LAZY_LOADING_REBUILD_TIME_RECOMMENDATION_LIMIT = 5000; -function getBootswatchThemePath(theme) { +function getBootswatchThemePath(theme: string) { return require.resolve(`bootswatch/dist/${theme}/bootstrap.min.css`); } -const SUPPORTED_THEMES_PATHS = { +const SUPPORTED_THEMES_PATHS: Record = { 'bootswatch-cerulean': getBootswatchThemePath('cerulean'), 'bootswatch-cosmo': getBootswatchThemePath('cosmo'), 'bootswatch-flatly': getBootswatchThemePath('flatly'), @@ -101,9 +119,85 @@ const ABOUT_MARKDOWN_DEFAULT = '# About\n' const MARKBIND_WEBSITE_URL = 'https://markbind.org/'; const MARKBIND_LINK_HTML = `MarkBind ${MARKBIND_VERSION}`; -class Site { - constructor(rootPath, outputPath, onePagePath, forceReload = false, - siteConfigPath = SITE_CONFIG_NAME, dev, backgroundBuildMode, postBackgroundBuildFunc) { +/* + * A page configuration object. + */ +type PageCreationConfig = { + externalScripts: string[], + frontmatter: FrontMatter, + layout: string, + pageSrc: string, + searchable: boolean, + faviconUrl?: string, + glob?: string, + globExclude?: string + title?: string, +}; + +type AddressablePage = { + frontmatter: FrontMatter, + layout: string, + searchable: string, + src: string, + externalScripts?: string[], + faviconUrl?: string, + title?: string, +}; + +type PageGenerationTask = { + mode: string, + pages: Page[] +}; + +type PageGenerationContext = { + startTime: Date, + numPagesGenerated: number, + numPagesToGenerate: number, + isCompleted: boolean, +}; + +type DeployOptions = { + branch: string, + message: string, + repo: string, + remote: string, + user?: { name: string; email: string; }, +}; + +export class Site { + dev: boolean; + rootPath: string; + outputPath: string; + tempPath: string; + siteAssetsDestPath: string; + pageTemplatePath: string; + pageTemplate: NunjucksTemplate; + pages: Page[]; + addressablePages: AddressablePage[]; + addressablePagesSource: string[]; + baseUrlMap: Set; + forceReload: boolean; + siteConfig!: SiteConfig; + siteConfigPath: string; + variableProcessor!: VariableProcessor; + layoutManager: any; // TODO: add LayoutManager when it has been migrated + pluginManager!: PluginManager; + siteLinkManager!: SiteLinkManager; + backgroundBuildMode: string | boolean; + stopGenerationTimeThreshold: Date; + postBackgroundBuildFunc: () => void; + onePagePath: string; + currentPageViewed: string; + currentOpenedPages: string[]; + toRebuild: Set; + externalManager!: ExternalManager; + buildAsset?: (this: any, arg: unknown) => Bluebird; + rebuildAffectedSourceFiles?: (this: any, arg: unknown) => Bluebird; + rebuildSourceFiles?: (this: any, arg: unknown) => Bluebird; + + constructor(rootPath: string, outputPath: string, onePagePath: string, forceReload = false, + siteConfigPath = SITE_CONFIG_NAME, dev: any, backgroundBuildMode: boolean, + postBackgroundBuildFunc: () => void) { this.dev = !!dev; this.rootPath = rootPath; @@ -124,24 +218,8 @@ class Site { this.baseUrlMap = new Set(); this.forceReload = forceReload; - /** - * @type {undefined | SiteConfig} - */ - this.siteConfig = undefined; this.siteConfigPath = siteConfigPath; - // Site wide variable processor - this.variableProcessor = undefined; - - // Site wide layout manager - this.layoutManager = undefined; - - // Site wide plugin manager - this.pluginManager = undefined; - - // Site wide link checker - this.siteLinkManager = undefined; - // Background build properties this.backgroundBuildMode = onePagePath && backgroundBuildMode; this.stopGenerationTimeThreshold = new Date(); @@ -160,12 +238,12 @@ class Site { * Util Methods */ - static async rejectHandler(error, removeFolders) { + static async rejectHandler(error: unknown, removeFolders: string[]) { logger.warn(error); try { await Promise.all(removeFolders.map(folder => fs.remove(folder))); } catch (err) { - logger.error(`Failed to remove generated files after error!\n${err.message}`); + logger.error(`Failed to remove generated files after error!\n${(err as Error).message}`); } } @@ -180,7 +258,7 @@ class Site { * @param normalizedUrl BaseUrl-less and extension-less url of the page * @return Boolean of whether the page needed to be rebuilt */ - changeCurrentPage(normalizedUrl) { + changeCurrentPage(normalizedUrl: string) { this.currentPageViewed = path.join(this.rootPath, normalizedUrl); if (this.toRebuild.has(this.currentPageViewed)) { @@ -199,10 +277,10 @@ class Site { /** * Changes the list of current opened pages - * @param {Array} normalizedUrls Collection of normalized url of pages taken from the clients + * @param normalizedUrls Collection of normalized url of pages taken from the clients * ordered from most-to-least recently opened */ - changeCurrentOpenedPages(normalizedUrls) { + changeCurrentOpenedPages(normalizedUrls: string[]) { if (!this.onePagePath) { return; } @@ -224,9 +302,8 @@ class Site { * Read and store the site config from site.json, overwrite the default base URL * if it's specified by the user. * @param baseUrl user defined base URL (if exists) - * @returns {Promise} */ - async readSiteConfig(baseUrl) { + async readSiteConfig(baseUrl?: string): Promise { try { const siteConfigPath = path.join(this.rootPath, this.siteConfigPath); const siteConfigJson = fs.readJsonSync(siteConfigPath); @@ -235,33 +312,19 @@ class Site { return this.siteConfig; } catch (err) { throw (new Error(`Failed to read the site config file '${this.siteConfigPath}' at` - + `${this.rootPath}:\n${err.message}\nPlease ensure the file exist or is valid`)); + + `${this.rootPath}:\n${(err as Error).message}\nPlease ensure the file exist or is valid`)); } } - listAssets(fileIgnore) { + listAssets(fileIgnore: Ignore) { const files = walkSync(this.rootPath, { directories: false }); return fileIgnore.filter(files); } - /** - * A page configuration object. - * @typedef {Object} PageCreationConfig - * @property {string} faviconUrl - * @property {string} pageSrc - * @property {string} title - * @property {string} layout - * @property {Object} frontmatter - * @property {boolean} searchable - * @property {Array} externalScripts - * / - /** * Create a Page object from the site and page creation config. - * @param {PageCreationConfig} config - * @returns {Page} */ - createPage(config) { + createPage(config: PageCreationConfig): Page { const sourcePath = path.join(this.rootPath, config.pageSrc); const resultPath = path.join(this.outputPath, fsUtil.setExtension(config.pageSrc, '.html')); @@ -290,6 +353,7 @@ class Site { ? 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js' : path.posix.join(baseAssetsPath, 'js', 'vue.min.js'), jQuery: path.posix.join(baseAssetsPath, 'js', 'jquery.min.js'), + layoutUserScriptsAndStyles: [], }, baseUrlMap: this.baseUrlMap, dev: this.dev, @@ -422,8 +486,8 @@ class Site { /** * Helper function for addDefaultLayoutToSiteConfig(). */ - static async writeToSiteConfig(config, configPath) { - const layoutObj = { glob: '**/*.md', layout: LAYOUT_DEFAULT_NAME }; + static async writeToSiteConfig(config: SiteConfig, configPath: string) { + const layoutObj: SiteConfigPage = { glob: '**/*.md', layout: LAYOUT_DEFAULT_NAME }; config.pages.push(layoutObj); await fs.outputJson(configPath, config); } @@ -446,10 +510,11 @@ class Site { .map(filePath => fsUtil.setExtension(filePath, '.html')); } - getPageGlobPaths(page, pagesExclude) { + getPageGlobPaths(page: SiteConfigPage, pagesExclude: string[]) { + const pageGlobs = page.glob ?? []; return walkSync(this.rootPath, { directories: false, - globs: Array.isArray(page.glob) ? page.glob : [page.glob], + globs: Array.isArray(pageGlobs) ? pageGlobs : [pageGlobs], ignore: [ CONFIG_FOLDER_NAME, SITE_FOLDER_NAME, @@ -465,7 +530,7 @@ class Site { const { pages, pagesExclude } = this.siteConfig; const pagesFromSrc = _.flatMap(pages.filter(page => page.src), page => (Array.isArray(page.src) ? page.src.map(pageSrc => ({ ...page, src: pageSrc })) - : [page])); + : [page])) as unknown as AddressablePage[]; const set = new Set(); const duplicatePages = pagesFromSrc .filter(page => set.size === set.add(page.src).size) @@ -480,15 +545,15 @@ class Site { searchable: page.searchable, layout: page.layout, frontmatter: page.frontmatter, - }))); + }))) as AddressablePage[]; /* Add pages collected from globs and merge properties for pages Page properties collected from src have priority over page properties from globs, while page properties from later entries take priority over earlier ones. */ - const filteredPages = {}; + const filteredPages: Record = {}; pagesFromGlobs.concat(pagesFromSrc).forEach((page) => { - const filteredPage = _.omitBy(page, _.isUndefined); + const filteredPage = _.omitBy(page, _.isUndefined) as AddressablePage; filteredPages[page.src] = page.src in filteredPages ? { ...filteredPages[page.src], ...filteredPage } : filteredPage; @@ -502,7 +567,6 @@ class Site { /** * Collects the base url map in the site/subsites - * @returns {*} */ collectBaseUrl() { const candidates = walkSync(this.rootPath, { directories: false }) @@ -519,7 +583,7 @@ class Site { * Set up the managers used with the configurations. */ buildManagers() { - const config = { + const config: ExternalManagerConfig & { externalManager: ExternalManager } = { baseUrlMap: this.baseUrlMap, baseUrl: this.siteConfig.baseUrl, rootPath: this.rootPath, @@ -530,6 +594,10 @@ class Site { intrasiteLinkValidation: this.siteConfig.intrasiteLinkValidation, codeLineNumbers: this.siteConfig.style.codeLineNumbers, plantumlCheck: this.siteConfig.plantumlCheck, + headerIdMap: {}, + siteLinkManager: this.siteLinkManager, + pluginManager: this.pluginManager, + externalManager: this.externalManager, }; this.siteLinkManager = new SiteLinkManager(config); config.siteLinkManager = this.siteLinkManager; @@ -556,7 +624,7 @@ class Site { content = fs.readFileSync(userDefinedVariablesPath, 'utf8'); } catch (e) { content = ''; - logger.warn(e.message); + logger.warn((e as Error).message); } /* @@ -571,7 +639,7 @@ class Site { this.variableProcessor.addUserDefinedVariable(base, 'MarkBind', MARKBIND_LINK_HTML); const $ = cheerio.load(content, { decodeEntities: false }); - $('variable,span').each((index, element) => { + $('variable,span').each((_index, element) => { const name = $(element).attr('name') || $(element).attr('id'); this.variableProcessor.renderAndAddUserDefinedVariable(base, name, $(element).html()); @@ -584,7 +652,7 @@ class Site { * if there is a change in the variables file * @param filePaths array of paths corresponding to files that have changed */ - collectUserDefinedVariablesMapIfNeeded(filePaths) { + collectUserDefinedVariablesMapIfNeeded(filePaths: string[]) { const variablesPath = path.resolve(this.rootPath, USER_VARIABLES_PATH); if (filePaths.includes(variablesPath)) { this.collectUserDefinedVariablesMap(); @@ -596,9 +664,8 @@ class Site { /** * Generate the website. * @param baseUrl user defined base URL (if exists) - * @returns {Promise} */ - async generate(baseUrl) { + async generate(baseUrl: string | undefined): Promise { const startTime = new Date(); // Create the .tmp folder for storing intermediate results. fs.emptydirSync(this.tempPath); @@ -611,7 +678,7 @@ class Site { try { await this.readSiteConfig(baseUrl); this.collectAddressablePages(); - await this.collectBaseUrl(); + this.collectBaseUrl(); this.collectUserDefinedVariablesMap(); await this.buildAssets(); await (this.onePagePath ? this.lazyBuildSourceFiles() : this.buildSourceFiles()); @@ -633,9 +700,9 @@ class Site { /** * Helper function for generate(). */ - calculateBuildTimeForGenerate(startTime, lazyWebsiteGenerationString) { + calculateBuildTimeForGenerate(startTime: Date, lazyWebsiteGenerationString: string) { const endTime = new Date(); - const totalBuildTime = (endTime - startTime) / 1000; + const totalBuildTime = (endTime.getTime() - startTime.getTime()) / 1000; logger.info(`Website generation ${lazyWebsiteGenerationString}complete! Total build time: ${ totalBuildTime}s`); @@ -664,7 +731,7 @@ class Site { /** * Adds all pages except the viewed pages to toRebuild, flagging them for lazy building later. */ - async lazyBuildAllPagesNotViewed(viewedPages) { + async lazyBuildAllPagesNotViewed(viewedPages: string | string[]) { const viewedPagesArray = Array.isArray(viewedPages) ? viewedPages : [viewedPages]; this.pages.forEach((page) => { const normalizedUrl = fsUtil.removeExtension(page.pageConfig.sourcePath); @@ -702,7 +769,7 @@ class Site { return fs.copy(lazyLoadingSpinnerHtmlFilePath, outputSpinnerHtmlFilePath); } - async _rebuildAffectedSourceFiles(filePaths) { + async _rebuildAffectedSourceFiles(filePaths: string | string[]) { if (this.backgroundBuildMode) { this.stopOngoingBuilds(); } @@ -723,7 +790,7 @@ class Site { } } - async _rebuildPagesBeingViewed(normalizedUrls) { + async _rebuildPagesBeingViewed(normalizedUrls: string[]) { const startTime = new Date(); const normalizedUrlArray = Array.isArray(normalizedUrls) ? normalizedUrls : [normalizedUrls]; const uniqueUrls = _.uniq(normalizedUrlArray); @@ -752,9 +819,9 @@ class Site { /** * Helper function for _rebuildPagesBeingViewed(). */ - static calculateBuildTimeForRebuildPagesBeingViewed(startTime) { + static calculateBuildTimeForRebuildPagesBeingViewed(startTime: Date) { const endTime = new Date(); - const totalBuildTime = (endTime - startTime) / 1000; + const totalBuildTime = (endTime.getTime() - startTime.getTime()) / 1000; return logger.info(`Lazy website regeneration complete! Total build time: ${totalBuildTime}s`); } @@ -773,9 +840,9 @@ class Site { /** * Generates pages that are marked to be built/rebuilt. - * @returns {Promise} A Promise that resolves once all pages are generated. + * @returns A Promise that resolves once all pages are generated. */ - async generatePagesMarkedToRebuild() { + async generatePagesMarkedToRebuild(): Promise { const pagesToRebuild = this.pages.filter((page) => { const normalizedUrl = fsUtil.removeExtension(page.pageConfig.sourcePath); return this.toRebuild.has(normalizedUrl); @@ -826,7 +893,7 @@ class Site { await this.buildSourceFiles(); } - async _buildMultipleAssets(filePaths) { + async _buildMultipleAssets(filePaths: string | string[]) { const filePathArray = Array.isArray(filePaths) ? filePaths : [filePaths]; const uniquePaths = _.uniq(filePathArray); const fileIgnore = ignore().add(this.siteConfig.ignore); @@ -837,7 +904,7 @@ class Site { logger.info('Assets built'); } - async _removeMultipleAssets(filePaths) { + async _removeMultipleAssets(filePaths: string | string[]) { const filePathArray = Array.isArray(filePaths) ? filePaths : [filePaths]; const uniquePaths = _.uniq(filePathArray); const fileRelativePaths = uniquePaths.map(filePath => path.relative(this.rootPath, filePath)); @@ -887,15 +954,17 @@ class Site { /** * Handles the rebuilding of modified pages */ - async handlePageReload(oldAddressablePages, oldPagesSrc, oldSiteConfig) { + async handlePageReload(oldAddressablePages: AddressablePage[], oldPagesSrc: string[], + oldSiteConfig: SiteConfig) { this.collectAddressablePages(); // Comparator for the _differenceWith comparison below - const isNewPage = (newPage, oldPage) => _.isEqual(newPage, oldPage) || newPage.src === oldPage.src; + const isNewPage = (newPage: AddressablePage, oldPage: AddressablePage) => + _.isEqual(newPage, oldPage) || newPage.src === oldPage.src; const addedPages = _.differenceWith(this.addressablePages, oldAddressablePages, isNewPage); const removedPages = _.differenceWith(oldAddressablePages, this.addressablePages, isNewPage) - .map(filePath => fsUtil.setExtension(filePath.src, '.html')); + .map(filePath => fsUtil.setExtension(filePath.src as string, '.html')); // Checks if any attributes of site.json requiring a global rebuild are modified const isGlobalConfigModified = () => !_.isEqual(oldSiteConfig.faviconPath, this.siteConfig.faviconPath) @@ -920,12 +989,8 @@ class Site { await this.writeSiteData(); } else { // Get pages with edited attributes but with the same src - const editedPages = _.differenceWith(this.addressablePages, oldAddressablePages, (newPage, oldPage) => { - if (!_.isEqual(newPage, oldPage)) { - return !oldPagesSrc.includes(newPage.src); - } - return true; - }); + const editedPages = _.differenceWith(this.addressablePages, oldAddressablePages, (newPage, oldPage) => + _.isEqual(newPage, oldPage) || !oldPagesSrc.includes(newPage.src)); this.updatePages(editedPages); const siteConfigDirectory = path.dirname(path.join(this.rootPath, this.siteConfigPath)); this.regenerateAffectedPages(editedPages.map(page => path.join(siteConfigDirectory, page.src))); @@ -935,7 +1000,7 @@ class Site { /** * Creates new pages and replaces the original pages with the updated version */ - updatePages(pagesToUpdate) { + updatePages(pagesToUpdate: AddressablePage[]) { pagesToUpdate.forEach((pageToUpdate) => { this.pages.forEach((page, index) => { if (page.pageConfig.src === pageToUpdate.src) { @@ -950,7 +1015,7 @@ class Site { /** * Handles the reloading of ignore attributes */ - async handleIgnoreReload(oldIgnore) { + async handleIgnoreReload(oldIgnore: string[]) { const assetsToRemove = _.difference(this.siteConfig.ignore, oldIgnore); if (!_.isEqual(oldIgnore, this.siteConfig.ignore)) { @@ -963,7 +1028,7 @@ class Site { /** * Handles the reloading of the style attribute if it has been modified */ - async handleStyleReload(oldStyle) { + async handleStyleReload(oldStyle: SiteConfigStyle) { if (!_.isEqual(oldStyle.bootstrapTheme, this.siteConfig.style.bootstrapTheme)) { await this.copyBootstrapTheme(true); logger.info('Updated bootstrap theme'); @@ -972,20 +1037,20 @@ class Site { /** * Checks if a specified file path is a dependency of a page - * @param {string} filePath file path to check - * @returns {boolean} whether the file path is a dependency of any of the site's pages + * @param filePath file path to check + * @returns whether the file path is a dependency of any of the site's pages */ - isDependencyOfPage(filePath) { + isDependencyOfPage(filePath: string): boolean { return this.pages.some(page => page.isDependency(filePath)) || fsUtil.ensurePosix(filePath).endsWith(USER_VARIABLES_PATH); } /** * Checks if a specified file path satisfies a src or glob in any of the page configurations. - * @param {string} filePath file path to check - * @returns {boolean} whether the file path is satisfies any glob + * @param filePath file path to check + * @returns whether the file path is satisfies any glob */ - isFilepathAPage(filePath) { + isFilepathAPage(filePath: string): boolean { const { pages, pagesExclude } = this.siteConfig; const relativeFilePath = fsUtil.ensurePosix(path.relative(this.rootPath, filePath)); const srcesFromPages = _.flatMap(pages.filter(page => page.src), @@ -1016,19 +1081,17 @@ class Site { /** * Maps an array of addressable pages to an array of Page object - * @param {Array} addressablePages - * @param {String} faviconUrl */ - mapAddressablePagesToPages(addressablePages, faviconUrl) { + mapAddressablePagesToPages(addressablePages: AddressablePage[], faviconUrl: string | undefined) { this.pages = addressablePages.map(page => this.createNewPage(page, faviconUrl)); } /** * Creates and returns a new Page with the given page config details and favicon url - * @param {Page} page config - * @param {String} faviconUrl of the page + * @param page config + * @param faviconUrl of the page */ - createNewPage(page, faviconUrl) { + createNewPage(page: AddressablePage, faviconUrl: string | undefined) { return this.createPage({ faviconUrl, pageSrc: page.src, @@ -1036,7 +1099,7 @@ class Site { layout: page.layout, frontmatter: page.frontmatter, searchable: page.searchable !== 'no', - externalScripts: page.externalScripts, + externalScripts: page.externalScripts || [], }); } @@ -1047,11 +1110,11 @@ class Site { /** * Runs the supplied page generation tasks according to the specified mode of each task. * A page generation task can be a sequential generation or an asynchronous generation. - * @param {Array} pageGenerationTasks Array of page generation tasks - * @returns {Promise} A Promise that resolves to a boolean which indicates whether the generation + * @param pageGenerationTasks Array of page generation tasks + * @returns A Promise that resolves to a boolean which indicates whether the generation * ran to completion */ - async runPageGenerationTasks(pageGenerationTasks) { + async runPageGenerationTasks(pageGenerationTasks: PageGenerationTask[]): Promise { const pagesCount = pageGenerationTasks.reduce((acc, task) => acc + task.pages.length, 0); const progressBar = new ProgressBar(`[:bar] :current / ${pagesCount} pages built`, { total: pagesCount }); progressBar.render(); @@ -1070,7 +1133,7 @@ class Site { if (task.mode === 'sequential') { isCompleted = await this.generatePagesSequential(task.pages, progressBar); } else { - isCompleted = await this.generatePagesAsyncThrottled(task.pages, progressBar); + isCompleted = await this.generatePagesAsyncThrottled(task.pages, progressBar) as boolean; } logger.removeProgressBar(); @@ -1082,12 +1145,12 @@ class Site { /** * Generate pages sequentially. That is, the pages are generated * one-by-one in order. - * @param {Array} pages Pages to be generated - * @param {ProgressBar} progressBar Progress bar of the overall generation process - * @returns {Promise} A Promise that resolves to a boolean which indicates whether the generation + * @param pages Pages to be generated + * @param progressBar Progress bar of the overall generation process + * @returns A Promise that resolves to a boolean which indicates whether the generation * ran to completion */ - async generatePagesSequential(pages, progressBar) { + async generatePagesSequential(pages: Page[], progressBar: any): Promise { const startTime = new Date(); let isCompleted = true; await sequentialAsyncForEach(pages, async (page) => { @@ -1116,14 +1179,14 @@ class Site { /** * Creates the supplied pages' page generation promises at a throttled rate. * This is done to avoid pushing too many callbacks into the event loop at once. (#1245) - * @param {Array} pages Pages to be generated - * @param {ProgressBar} progressBar Progress bar of the overall generation process - * @returns {Promise} A Promise that resolves to a boolean which indicates whether the generation + * @param pages Pages to be generated + * @param progressBar Progress bar of the overall generation process + * @returns A Promise that resolves to a boolean which indicates whether the generation * ran to completion */ - generatePagesAsyncThrottled(pages, progressBar) { + generatePagesAsyncThrottled(pages: Page[], progressBar: any): Promise { return new Promise((resolve, reject) => { - const context = { + const context: PageGenerationContext = { startTime: new Date(), numPagesGenerated: 0, numPagesToGenerate: pages.length, @@ -1169,7 +1232,8 @@ class Site { /** * Helper function for generatePagesAsyncThrottled(). */ - generateProgressBarStatus(progressBar, context, pageGenerationQueue, resolve) { + generateProgressBarStatus(progressBar: any, context: PageGenerationContext, + pageGenerationQueue: (() => Promise)[], resolve: ((arg0: boolean) => any)) { // Post-generate guard to ensure no new callbacks are executed on stop if (this.backgroundBuildMode && context.startTime < this.stopGenerationTimeThreshold) { if (context.isCompleted) { @@ -1184,7 +1248,7 @@ class Site { context.numPagesGenerated += 1; if (pageGenerationQueue.length) { - pageGenerationQueue.pop()(); + pageGenerationQueue.pop()!(); } else if (context.numPagesGenerated === context.numPagesToGenerate) { resolve(true); } @@ -1228,7 +1292,7 @@ class Site { await landingPage.generate(this.externalManager); } - async regenerateAffectedPages(filePaths) { + async regenerateAffectedPages(filePaths: string[]) { const startTime = new Date(); const shouldRebuildAllPages = this.collectUserDefinedVariablesMapIfNeeded(filePaths) || this.forceReload; @@ -1237,7 +1301,7 @@ class Site { } this._setTimestampVariable(); - let openedPagesToRegenerate = []; + let openedPagesToRegenerate: Page[] = []; const asyncPagesToRegenerate = this.pages.filter((page) => { const doFilePathsHaveSourceFiles = filePaths.some(filePath => page.isDependency(filePath)); @@ -1305,9 +1369,9 @@ class Site { /** * Helper function for regenerateAffectedPages(). */ - calculateBuildTimeForRegenerateAffectedPages(startTime) { + calculateBuildTimeForRegenerateAffectedPages(startTime: Date) { const endTime = new Date(); - const totalBuildTime = (endTime - startTime) / 1000; + const totalBuildTime = (endTime.getTime() - startTime.getTime()) / 1000; logger.info(`Website regeneration complete! Total build time: ${totalBuildTime}s`); if (!this.onePagePath && totalBuildTime > LAZY_LOADING_REBUILD_TIME_RECOMMENDATION_LIMIT) { logger.info('Your pages took quite a while to rebuild...' @@ -1383,7 +1447,7 @@ class Site { * Copies bootstrapTheme to the assets folder if a valid bootstrapTheme is specified * @param {Boolean} isRebuild only true if it is a rebuild */ - copyBootstrapTheme(isRebuild) { + copyBootstrapTheme(isRebuild: boolean) { const { bootstrapTheme } = this.siteConfig.style; /** @@ -1405,9 +1469,9 @@ class Site { /** * Writes the site data to siteData.json - * @param {boolean} verbose Flag to emit logs of the operation + * @param verbose Flag to emit logs of the operation */ - async writeSiteData(verbose = true) { + async writeSiteData(verbose: boolean = true) { const siteDataPath = path.join(this.outputPath, SITE_DATA_NAME); const siteData = { enableSearch: this.siteConfig.enableSearch, @@ -1431,8 +1495,8 @@ class Site { } } - deploy(ciTokenVar) { - const defaultDeployConfig = { + deploy(ciTokenVar: string | boolean) { + const defaultDeployConfig: DeployOptions = { branch: 'gh-pages', message: 'Site Update.', repo: '', @@ -1445,8 +1509,8 @@ class Site { /** * Helper function for deploy(). Returns the ghpages link where the repo will be hosted. */ - async generateDepUrl(ciTokenVar, defaultDeployConfig) { - const publish = Promise.promisify(ghpages.publish); + async generateDepUrl(ciTokenVar: boolean | string, defaultDeployConfig: DeployOptions) { + const publish = Bluebird.promisify(ghpages.publish); await this.readSiteConfig(); const depOptions = await this.getDepOptions(ciTokenVar, defaultDeployConfig, publish); return Site.getDepUrl(depOptions, defaultDeployConfig); @@ -1455,17 +1519,20 @@ class Site { /** * Helper function for deploy(). Set the options needed to be used by ghpages.publish. */ - async getDepOptions(ciTokenVar, defaultDeployConfig, publish) { + async getDepOptions(ciTokenVar: boolean | string, defaultDeployConfig: DeployOptions, + publish: (basePath: string, options: DeployOptions) => Bluebird) { const basePath = this.siteConfig.deploy.baseDir || this.outputPath; if (!fs.existsSync(basePath)) { throw new Error( 'The site directory does not exist. Please build the site first before deploy.'); } - const options = {}; - options.branch = this.siteConfig.deploy.branch || defaultDeployConfig.branch; - options.message = this.siteConfig.deploy.message || defaultDeployConfig.message; + const options: DeployOptions = { + branch: this.siteConfig.deploy.branch || defaultDeployConfig.branch, + message: this.siteConfig.deploy.message || defaultDeployConfig.message, + repo: this.siteConfig.deploy.repo || defaultDeployConfig.repo, + remote: '', + }; options.message = options.message.concat(' [skip ci]'); - options.repo = this.siteConfig.deploy.repo || defaultDeployConfig.repo; if (ciTokenVar) { const ciToken = _.isBoolean(ciTokenVar) ? 'GITHUB_TOKEN' : ciTokenVar; @@ -1521,7 +1588,7 @@ class Site { /** * Extract repo slug from user-specified repo URL so that we can include the access token */ - static extractRepoSlug(repo, ciRepoSlug) { + static extractRepoSlug(repo: string, ciRepoSlug: string | undefined) { if (!repo) { return ciRepoSlug; } @@ -1538,7 +1605,7 @@ class Site { /** * Helper function for deploy(). */ - static getDepUrl(options, defaultDeployConfig) { + static getDepUrl(options: DeployOptions, defaultDeployConfig: DeployOptions) { const git = simpleGit({ baseDir: process.cwd() }); options.remote = defaultDeployConfig.remote; return Site.getDeploymentUrl(git, options); @@ -1547,13 +1614,13 @@ class Site { /** * Gets the deployed website's url, returning null if there was an error retrieving it. */ - static async getDeploymentUrl(git, options) { + static async getDeploymentUrl(git: SimpleGit, options: DeployOptions) { const HTTPS_PREAMBLE = 'https://'; const SSH_PREAMBLE = 'git@github.com:'; const GITHUB_IO_PART = 'github.io'; // https://.github.io// - function constructGhPagesUrl(remoteUrl) { + function constructGhPagesUrl(remoteUrl: string) { if (!remoteUrl) { return null; } @@ -1580,8 +1647,8 @@ class Site { const promises = [cnamePromise, remoteUrlPromise]; try { - const promiseResults = await Promise.all(promises); - const generateGhPagesUrl = (results) => { + const promiseResults: string[] = await Promise.all(promises) as string[]; + const generateGhPagesUrl = (results: string[]) => { const cname = results[0]; const remoteUrl = results[1]; if (cname) { @@ -1600,7 +1667,7 @@ class Site { } _setTimestampVariable() { - const options = { + const options: Intl.DateTimeFormatOptions = { weekday: 'short', year: 'numeric', month: 'short', @@ -1611,6 +1678,21 @@ class Site { const time = new Date().toLocaleTimeString(this.siteConfig.locale, options); this.variableProcessor.addUserDefinedVariableForAllSites('timestamp', time); } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + async rebuildPagesBeingViewed(_currentPageViewed: string) { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + async removeAsset(_removedPageFilePaths: string | string[]) { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + async backgroundBuildNotViewedFiles(_arg0?: any, _arg1?: any) { + throw new Error('Method not implemented.'); + } } /** @@ -1621,31 +1703,34 @@ class Site { * Build/copy assets that are specified in filePaths * @param filePaths a single path or an array of paths corresponding to the assets to build */ -Site.prototype.buildAsset = delay(Site.prototype._buildMultipleAssets, 1000); +Site.prototype.buildAsset = delay(Site.prototype._buildMultipleAssets as () => Bluebird, 1000); -Site.prototype.rebuildPagesBeingViewed = delay(Site.prototype._rebuildPagesBeingViewed, 1000); +Site.prototype.rebuildPagesBeingViewed = delay( + Site.prototype._rebuildPagesBeingViewed as () => Bluebird, 1000); /** * Rebuild pages that are affected by changes in filePaths * @param filePaths a single path or an array of paths corresponding to the files that have changed */ -Site.prototype.rebuildAffectedSourceFiles = delay(Site.prototype._rebuildAffectedSourceFiles, 1000); +Site.prototype.rebuildAffectedSourceFiles = delay( + Site.prototype._rebuildAffectedSourceFiles as () => Bluebird, 1000); /** * Rebuild all pages * @param filePaths a single path or an array of paths corresponding to the files that have changed */ -Site.prototype.rebuildSourceFiles = delay(Site.prototype._rebuildSourceFiles, 1000); +Site.prototype.rebuildSourceFiles = delay( + Site.prototype._rebuildSourceFiles as () => Bluebird, 1000); /** * Remove assets that are specified in filePaths * @param filePaths a single path or an array of paths corresponding to the assets to remove */ -Site.prototype.removeAsset = delay(Site.prototype._removeMultipleAssets, 1000); +Site.prototype.removeAsset = delay( + Site.prototype._removeMultipleAssets as () => Bluebird, 1000); /** * Builds pages that are yet to build/rebuild in the background */ -Site.prototype.backgroundBuildNotViewedFiles = delay(Site.prototype._backgroundBuildNotViewedFiles, 1000); - -module.exports = Site; +Site.prototype.backgroundBuildNotViewedFiles = delay( + Site.prototype._backgroundBuildNotViewedFiles as () => Bluebird, 1000); diff --git a/packages/core/src/variables/VariableProcessor.ts b/packages/core/src/variables/VariableProcessor.ts index 4db1cb102f..b7b7abc26d 100644 --- a/packages/core/src/variables/VariableProcessor.ts +++ b/packages/core/src/variables/VariableProcessor.ts @@ -92,7 +92,12 @@ export class VariableProcessor { * Renders the variable in addition to adding it, unlike {@link addUserDefinedVariable}. * This is to allow using previously declared site variables in site variables declared later on. */ - renderAndAddUserDefinedVariable(site: string, name: string, value: any) { + renderAndAddUserDefinedVariable(site: string, name: string | undefined, value: any) { + if (name === undefined) { + logger.warn('You have a variable with no name! This variable will be ignored.'); + return; + } + const renderedVal = this.variableRendererMap[site].renderString(value, this.userDefinedVariablesMap[site], new PageSources()); this.addUserDefinedVariable(site, name, renderedVal); diff --git a/packages/core/test/unit/Site.test.js b/packages/core/test/unit/Site.test.js index d69c0462aa..b826679feb 100644 --- a/packages/core/test/unit/Site.test.js +++ b/packages/core/test/unit/Site.test.js @@ -1,8 +1,8 @@ const path = require('path'); const fs = require('fs-extra'); const ghpages = require('gh-pages'); -const Site = require('../../src/Site'); const { Template } = require('../../src/Site/template'); +const { Site } = require('../../src/Site'); const { INDEX_MD_DEFAULT,