diff --git a/docs/_markbind/layouts/userGuide.md b/docs/_markbind/layouts/userGuide.md index 13de7b6082..c1fecf6649 100644 --- a/docs/_markbind/layouts/userGuide.md +++ b/docs/_markbind/layouts/userGuide.md @@ -30,6 +30,7 @@ * [Making the Site Searchable]({{baseUrl}}/userGuide/makingTheSiteSearchable.html) * [Applying Themes]({{baseUrl}}/userGuide/themes.html) * [Deploying the Site]({{baseUrl}}/userGuide/deployingTheSite.html) + * [Versioning]({{baseUrl}}/userGuide/versioning.html) * [MarkBind in the Project Workflow]({{baseUrl}}/userGuide/markBindInTheProjectWorkflow.html) * **References** :expanded: * [CLI Commands]({{baseUrl}}/userGuide/cliCommands.html) diff --git a/docs/userGuide/cliCommands.md b/docs/userGuide/cliCommands.md index 74d0becf05..41b3df3ca5 100644 --- a/docs/userGuide/cliCommands.md +++ b/docs/userGuide/cliCommands.md @@ -9,20 +9,23 @@ ### Overview An overview of MarkBind's Command Line Interface (CLI) can be referenced with `markbind --help`: -``` + +```bash $ markbind --help Usage: markbind Options: - -V, --version output the version number - -h, --help output usage information + -V, --version output the version number + -h, --help output usage information Commands: - init|i [options] [root] init a markbind website project - serve|s [options] [root] build then serve a website from a directory - build|b [options] [root] [output] build a website - deploy|d [options] deploy the site to the repo's GitHub pages + init|i [options] [root] init a markbind website project + serve|s [options] [root] build then serve a website from a directory + build|b [options] [root] [output] build a website + archive|ar [options] archive the current version of the site + deploy|d [options] deploy the site to the repo's GitHub pages ``` +
@@ -36,6 +39,7 @@ Usage: markbind **Description:** Initializes a directory into a MarkBind site by creating a skeleton structure for the website which includes a `index.md` and a `site.json`. **Arguments:** + * `[root]`
Root directory. Default is the current directory.
{{ icon_example }} `./myWebsite` @@ -51,6 +55,7 @@ Usage: markbind When initialising MarkBind, change the template that you start with. See [templates](templates.html). {{ icon_examples }} + * `markbind init` : Initializes the site in the current working directory. * `markbind init ./myWebsite` : Initializes the site in `./myWebsite` directory. * `markbind init --convert --template minimal`: Converts the GitHub wiki or `docs` folder in the current working directory into a minimal MarkBind website. @@ -67,6 +72,7 @@ Usage: markbind **Alias:** `markbind s` **Description:** Does the following steps: + 1. Builds the site and puts the generated files in a directory named `_site`. 1. Starts a web server instance locally and makes the site available at `http://127.0.0.1:8080`. 1. Opens a live preview of the website. @@ -76,6 +82,7 @@ Usage: markbind **Arguments:** + * `[root]`
Root directory. The default is the directory where this command was executed.
{{ icon_example }} `./myWebsite` @@ -100,7 +107,7 @@ The caveat is that not building all pages during the initial process, or not reb * `-b`, `--background-build` **[BETA]**
If `--one-page` is specified, this mode enhances the single-page serve by building the pages that are not yet built or marked to be rebuilt in the background. - + You can still edit the pages during the background build. When MarkBind detects changes to the source files, the background build will stop, rebuild the files affected, then resumes the background build with the remaining pages. @@ -116,13 +123,17 @@ The caveat is that not building all pages during the initial process, or not reb Force live reload to process all files in the site, instead of just the relevant files. This option is useful when you are modifying a file that is not a file type monitored by the live preview feature. * `-p `, `--port `
- Serve the website in the specified port. + Serve the website in the specified port. +* `-v [versionNames...]`, `--versions [versionNames...]`
+ Specify versions to be served, separated by spaces. If the flag is used without specification, serve no versions. Using this option overrides the versions settings in [`site.json`](siteJsonFile.md). {{ icon_examples }} + * `markbind serve` * `markbind serve ./myWebsite` * `markbind serve -p 8888 -s otherSite.json` +* `markbind serve -n -v LTS 1.0` : Serve the site without opening a live preview in the browser, and also serve the archived version named "LTS" and "1.0". @@ -138,6 +149,7 @@ The caveat is that not building all pages during the initial process, or not reb **Description:** Generates the site to the directory named `_site` in the current directory. **Arguments:** + * `[output]`
Put the generated files in the specified directory
{{ icon_example }} `../myOutDir` @@ -158,10 +170,65 @@ The caveat is that not building all pages during the initial process, or not reb Specify the site config file (default: `site.json`)
{{ icon_example }} `-s otherSite.json` +* `-v [versionNames...]`, `--versions [versionNames...]`
+ Specify versions to be kept in the generated site, separated by spaces. If the flag is used without specification, keep no versions. Using this option overrides the versions settings in [site.json](siteJsonFile.md). + **{{ icon_examples }}** + * `markbind build` * `markbind build ./myWebsite ./myOutDir` * `markbind build ./stagingDir --baseUrl staging` +* `markbind build -v v2.1.1` : Build the site with the version named 'v2.1.1' in addition to the current version + + + +
+ +### `archive` Command +
+ +**Format:** `markbind archive [options]` + +**Alias:** `markbind ar ` + +**Description:** Does the following steps: + +1. Builds the current site, ignoring previously archived versions. +1. Updates or creates a `versions.json` file to track the newly archived version. +1. Puts the generated files in the specified `archivePath` folder (By default, the archive path is "version/") + +**Arguments:** + +* ``
+ The name of the version. This is required, and names must be unique; using the same name and archivePath will result in the previous archived files being overwritten.
+ {{ icon_example }} `v1`, `v1.1.1`, `sem1-2022` + + + +**Options** :fas-cogs: + +* `-s `, `--site-config `
+ Specify the site config file (default: `site.json`)
+ {{ icon_example }} `-s otherSite.json` +* `-ap `, `--archive-path `
+ All archived versions are stored in the folder ``. If not specified, the archive path is `version/${versionName}`
+ {{ icon_example }} `-ap custom_archive_path` + +
+ + + Warning: If the folder at `` already exists, the contents will be overwritten and your previous files may be lost. Only do so if you need to replace all the archived files with the current site files. + + (Also note that you cannot save a version with the same name into a different archive path.) + +
+ +**{{ icon_examples }}** + +* `markbind archive v1`: Stores the archived site in the directory `./version/v1` as the version named 'v1' +* `markbind archive version_1 -ap custom_archive_path`: Stores the archived site in the directory `./custom_archive_path`, and the version is named 'version_1'. + +%%{{ icon_info }} Related: [User Guide: Site Versioning](versioning.md).%%
@@ -200,5 +267,6 @@ The caveat is that not building all pages during the initial process, or not reb **Description:** Prints a summary of MarkBind commands or a detailed usage guide for the given `command`. {{ icon_examples }} + * `markbind --help` : Prints a summary of MarkBind commands. * `markbind serve --help` : Prints a detailed usage guide for the `serve` command. diff --git a/docs/userGuide/deployingTheSite.md b/docs/userGuide/deployingTheSite.md index c9754763d1..3bb6613e82 100644 --- a/docs/userGuide/deployingTheSite.md +++ b/docs/userGuide/deployingTheSite.md @@ -545,4 +545,4 @@ For more information on Surge, you may refer to [Surge's docs](https://surge.sh/ {% from "njk/common.njk" import previous_next %} -{{ previous_next('themes', 'markBindInTheProjectWorkflow') }} +{{ previous_next('themes', 'versioning') }} diff --git a/docs/userGuide/glossary.md b/docs/userGuide/glossary.md index 81638866e5..5ef9e06295 100644 --- a/docs/userGuide/glossary.md +++ b/docs/userGuide/glossary.md @@ -10,7 +10,7 @@ **_Live preview_** is: - Regeneration of affected content upon any change to source files, then reloading the updated site in the Browser. -- Regeneration will also occur upon any modification to attributes in `site.json` with the exception of [`baseUrl`](siteJsonFile.md#baseurl). +- Regeneration will also occur upon any modification to attributes in `site.json` with the exception of the [`baseUrl`](siteJsonFile.md#baseurl) and [`versions`](siteJsonFile.md#versions) attributes. - Copying assets to the site output folder. diff --git a/docs/userGuide/markBindInTheProjectWorkflow.md b/docs/userGuide/markBindInTheProjectWorkflow.md index d8fc23958f..5782745bbc 100644 --- a/docs/userGuide/markBindInTheProjectWorkflow.md +++ b/docs/userGuide/markBindInTheProjectWorkflow.md @@ -66,4 +66,4 @@ To convert your existing project, follow these steps: {% from "njk/common.njk" import previous_next %} -{{ previous_next('deployingTheSite', '') }} +{{ previous_next('versioning', '') }} diff --git a/docs/userGuide/siteJsonFile.md b/docs/userGuide/siteJsonFile.md index 00ab04e275..c1822e7ac0 100644 --- a/docs/userGuide/siteJsonFile.md +++ b/docs/userGuide/siteJsonFile.md @@ -75,6 +75,7 @@ Here is a typical `site.json` file: "intrasiteLinkValidation": { "enabled": false }, + "versions" : ["v1"] "plantumlCheck": true } ``` @@ -275,6 +276,17 @@ To disable this validation **entirely**, you may add the following to `site.json
+#### **`versions`** + +**A list of version names to deploy by default when building and serving the site**. If this list is not present, by default no versions will be deployed. (You can override these settings by passing the `--versions` flag to build and serve – see the [MarkBind CLI page](cliCommands.md) for more.) + +The version names to specify should be the same ones as in `versions.json`. Refer to the [Site Versioning](versioning.md) for more details. + + ```js + ... + "versions": ["v1.1.2"] // build/deploy just v1.1.2 by default + ... + ``` #### **`plantumlCheck`** **Toggle whether to display a warning about PlantUML's prerequisite.** By default, MarkBind will check if you have Graphviz installed when you are using PlantUML diagrams. diff --git a/docs/userGuide/versioning.md b/docs/userGuide/versioning.md new file mode 100644 index 0000000000..038d687c8e --- /dev/null +++ b/docs/userGuide/versioning.md @@ -0,0 +1,99 @@ +{% set title = "Site Versioning" %} +{% set filename = "versioning" %} +{{ title }} + + + title: "User Guide: {{ title }}" + layout: userGuide.md + + + +[_User Guide → {{ title }}_]({{ filename }}.html) + + +# {{ title }} + +
+ +Site versioning is key for documentation use, and websites may want to keep past versions for archival purposes. MarkBind can help you easily archive your site. +
+ +## Quickstart + +If you wanted to archive your site for the first time, you might use the following command. + + ```bash + $ markbind archive v1 // archive the current site with the name v1 into the folder version/v1 + ``` + +Make whatever changes you want to your site without affecting this saved version. Then, run: + + ```bash + $ markbind serve -v v1 // serve the site as well as the archived version named v1 + ``` + +Your served site will open automatically. By adding version/v1 to the url in your browser (for example from http://127.0.0.1:8080 to http://127.0.0.1:8080/version/v1), you will view the archived version of your site. Intralinks in the versioned site will only lead to the versioned site links. + +To deploy your site with your archived site: + + ```bash + $ markbind build -v v1 // generate site with the archived version named v1 + $ markbind deploy + ``` + +You can save which versions to automatically be served/deployed in [site.json](siteJsonFile.md#versions). + +## More on archiving + +MarkBind allows you to easily save a version of the site you've built to be hosted at the same site with a modified URL using a [single CLI command](cliCommands.md#archive-command). All intralinks within the archived site will point to the respective archived pages. By default, the archived site is stored in a folder `version/`, but you may specify your own archivePath. + +For example, if your site's base URL relative to your domain is `my_site`, and you archive a version named `v1`, then by navigating to the URL `/my_site/version/v1/` you can access the archived version of `someFile`. + +A `versions.json` file will be created to track the archived sites you have made, and to exclude the archived sites from being re-archived the next time you make a new version. This file is **automatically updated** every time you archive a version. + + + +Modify versions.json with caution as it may result in unnecessary files being included or necessary files being excluded. + +* You may safely change the `versionName` of a version, **provided that it is unique** in versions.json. If you have specified versions to deploy in `site.json`, make sure you update the [versions property](siteJsonFile.md#versions) there as well. + +* The baseUrl is used when setting the intra-site links; if you later change the baseUrl, previously saved versions with the past baseUrl will not be built/deployed even if specified because it would be a broken implementation. + + + +```json {heading="Example of a versions.json file"} +{ + "versions": [ + { + "versionName": "v1", + "buildVer": "3.1.1", + "archivePath": "version/v1", + "baseUrl": "/previousUrl" + }, + { + "versionName": "v2", + "buildVer": "3.1.1", + "archivePath": "version/v2", + "baseUrl": "/markbind" + }, + { + "versionName": "v3", + "buildVer": "3.1.1", + "archivePath": "version/v3", + "baseUrl": "/markbind" + } + ] +} + +``` + + + +## Working with sites with multiple versions + +You may not always want to build all your saved versions. To specify the default versions to build, add a [versions property](siteJsonFile.md#versions) in your `site.json` file. + +You may also specify which versions to build when using the build and serve cli commands([more information](cliCommands.md)). + +{% from "njk/common.njk" import previous_next %} +{{ previous_next('deployingTheSite', 'markBindInTheProjectWorkflow') }} diff --git a/packages/cli/index.js b/packages/cli/index.js index 7558af938f..6a8eb2192e 100755 --- a/packages/cli/index.js +++ b/packages/cli/index.js @@ -98,6 +98,8 @@ program .option('-p, --port ', 'port for server to listen on (Default is 8080)') .option('-s, --site-config ', 'specify the site config file (default: site.json)') .option('-d, --dev', 'development mode, enabling live & hot reload for frontend source files.') + .option('-v, --versions [versionNames...]', + 'specify versions to be deployed. if flag is used without specification, deploy no versions') .action((userSpecifiedRoot, options) => { if (options.dev) { logger.useDebugConsole(); @@ -262,7 +264,7 @@ program serverConfig.open = serverConfig.open && `${config.baseUrl}/`; } - return site.generate(); + return site.generate(undefined, options.versions); }) .then(() => { const watcher = chokidar.watch(rootFolder, { @@ -298,6 +300,8 @@ program .option('--baseUrl [baseUrl]', 'optional flag which overrides baseUrl in site.json, leave argument empty for empty baseUrl') .option('-s, --site-config ', 'specify the site config file (default: site.json)') + .option('-v, --versions [versionNames...]', + 'specify versions to be deployed. if flag is used without specification, deploy no versions') .description('build a website') .action((userSpecifiedRoot, output, options) => { // if --baseUrl contains no arguments (options.baseUrl === true) then set baseUrl to empty string @@ -311,13 +315,31 @@ program const defaultOutputRoot = path.join(rootFolder, '_site'); const outputFolder = output ? path.resolve(process.cwd(), output) : defaultOutputRoot; new Site(rootFolder, outputFolder, undefined, undefined, options.siteConfig) - .generate(baseUrl) + .generate(baseUrl, options.versions) .then(() => { logger.info('Build success!'); }) .catch(handleError); }); +program + .command('archive ') + .alias('ar') + .option('-s, --site-config ', 'specify the site config file (default: site.json)') + .option('-ap, --archive-path ', 'specify a custom path to archive the site at') + .description('archive a version of the site, which is not affected by later changes to the site') + .action((versionName, options) => { + const archivePath = options.archivePath || `version/${versionName}`; + const rootFolder = path.resolve(process.cwd()); + const outputFolder = path.join(rootFolder, archivePath); + new Site(rootFolder, outputFolder, undefined, undefined, options.siteConfig) + .archive(versionName, archivePath) + .then(() => { + logger.info(`Successfully archived ${versionName} at ${archivePath}`); + }) + .catch(handleError); + }); + program .command('deploy') .alias('d') diff --git a/packages/cli/test/functional/test_site/_markbind/versions.json b/packages/cli/test/functional/test_site/_markbind/versions.json new file mode 100644 index 0000000000..98d354aed6 --- /dev/null +++ b/packages/cli/test/functional/test_site/_markbind/versions.json @@ -0,0 +1,3 @@ +{ + "versions": [] +} diff --git a/packages/cli/test/functional/test_site_algolia_plugin/_markbind/versions.json b/packages/cli/test/functional/test_site_algolia_plugin/_markbind/versions.json new file mode 100644 index 0000000000..98d354aed6 --- /dev/null +++ b/packages/cli/test/functional/test_site_algolia_plugin/_markbind/versions.json @@ -0,0 +1,3 @@ +{ + "versions": [] +} diff --git a/packages/cli/test/functional/test_site_special_tags/_markbind/versions.json b/packages/cli/test/functional/test_site_special_tags/_markbind/versions.json new file mode 100644 index 0000000000..98d354aed6 --- /dev/null +++ b/packages/cli/test/functional/test_site_special_tags/_markbind/versions.json @@ -0,0 +1,3 @@ +{ + "versions": [] +} diff --git a/packages/core/src/Site/SiteConfig.js b/packages/core/src/Site/SiteConfig.js index f841bc1100..0c43b122d2 100644 --- a/packages/core/src/Site/SiteConfig.js +++ b/packages/core/src/Site/SiteConfig.js @@ -114,6 +114,8 @@ class SiteConfig { */ this.plantumlCheck = siteConfigJson.plantumlCheck !== undefined ? siteConfigJson.plantumlCheck : true; // check PlantUML's prerequisite by default + this.versions = siteConfigJson.versions !== undefined + ? siteConfigJson.versions : []; } } diff --git a/packages/core/src/Site/constants.js b/packages/core/src/Site/constants.js index dae7f1b0a4..99ce2e127f 100644 --- a/packages/core/src/Site/constants.js +++ b/packages/core/src/Site/constants.js @@ -12,6 +12,7 @@ module.exports = { PAGE_TEMPLATE_NAME: 'page.njk', SITE_CONFIG_NAME: 'site.json', SITE_DATA_NAME: 'siteData.json', + VERSIONS_DATA_NAME: '_markbind/versions.json', LAYOUT_SITE_FOLDER_NAME: 'layouts', LAZY_LOADING_SITE_FILE_NAME: 'LazyLiveReloadLoadingSite.html', LAZY_LOADING_BUILD_TIME_RECOMMENDATION_LIMIT: 30000, diff --git a/packages/core/src/Site/index.js b/packages/core/src/Site/index.js index 0c0b28512e..adbc9c48cf 100644 --- a/packages/core/src/Site/index.js +++ b/packages/core/src/Site/index.js @@ -64,6 +64,7 @@ const { PAGE_TEMPLATE_NAME, SITE_CONFIG_NAME, SITE_DATA_NAME, + VERSIONS_DATA_NAME, SITE_FOLDER_NAME, TEMP_FOLDER_NAME, TEMPLATE_SITE_ASSET_FOLDER_NAME, @@ -135,6 +136,12 @@ class Site { this.siteConfig = undefined; this.siteConfigPath = siteConfigPath; + /** + * Archived version information + * @type {undefined | Object} + */ + this.versionData = undefined; + // Site wide variable processor this.variableProcessor = undefined; @@ -641,7 +648,7 @@ class Site { * @param baseUrl user defined base URL (if exists) * @returns {Promise} */ - async generate(baseUrl) { + async generate(baseUrl, versionsToGenerate = false) { const startTime = new Date(); // Create the .tmp folder for storing intermediate results. fs.emptydirSync(this.tempPath); @@ -653,17 +660,13 @@ class Site { try { await this.readSiteConfig(baseUrl); - this.collectAddressablePages(); - await this.collectBaseUrl(); - this.collectUserDefinedVariablesMap(); - await this.buildAssets(); - await (this.onePagePath ? this.lazyBuildSourceFiles() : this.buildSourceFiles()); - await this.copyCoreWebAsset(); - await this.copyBootstrapTheme(false); - await this.copyFontAwesomeAsset(); - await this.copyOcticonsAsset(); - await this.copyMaterialIconsAsset(); - await this.writeSiteData(); + this.ignoreVersionFiles(''); + + // Also build requested versions + await this.copySpecifiedVersions(versionsToGenerate); + + await this.buildSiteHelper(); + this.calculateBuildTimeForGenerate(startTime, lazyWebsiteGenerationString); if (this.backgroundBuildMode) { this.backgroundBuildNotViewedFiles(); @@ -673,6 +676,42 @@ class Site { } } + /** + * Filters the versions based on the options passed, and copies the desired versions for later deployment + */ + async copySpecifiedVersions(versionsToGenerate) { + this.versionData = await this.readVersionData(); + const desiredVersions = this.versionData.versions + .filter(vers => vers.baseUrl === this.siteConfig.baseUrl); + + if (versionsToGenerate === true) { + // copy no versions if the version flag is passed without arguments + } else if (versionsToGenerate === false) { + await this.copyVersions(desiredVersions + .filter(vers => this.siteConfig.versions.includes(vers.versionName))); + } else { + await this.copyVersions(desiredVersions + .filter(vers => versionsToGenerate.includes(vers.versionName))); + } + } + + /** + * Holds the work for generating a site from scratch. + */ + async buildSiteHelper() { + this.collectAddressablePages(); + await this.collectBaseUrl(); + this.collectUserDefinedVariablesMap(); + await this.buildAssets(); + await (this.onePagePath ? this.lazyBuildSourceFiles() : this.buildSourceFiles()); + await this.copyCoreWebAsset(); + await this.copyBootstrapTheme(false); + await this.copyFontAwesomeAsset(); + await this.copyOcticonsAsset(); + await this.copyMaterialIconsAsset(); + await this.writeSiteData(); + } + /** * Helper function for generate(). */ @@ -1472,6 +1511,146 @@ class Site { } } + /** + * Builds and archives the current version of the site. + * + * @param {string} versionName the name of the version + * @param {string} archivePath the path to the folder to store the archives in + */ + async archive(versionName, archivePath) { + await this.readSiteConfig(); + + // Save version data + this.versionData = await this.writeVersionsFile(versionName, archivePath); + // Exclude versioned files from archiving. + this.ignoreVersionFiles(''); + + // Used to get accurate intralinks within the archived site: + const archivedBaseUrl = `${this.siteConfig.baseUrl}/${archivePath}`; + this.siteConfig.baseUrl = archivedBaseUrl; + + // Create the .tmp folder for storing intermediate results. + fs.emptydirSync(this.tempPath); + // Clean the output folder; create it if not exist. + fs.emptydirSync(this.outputPath); + + try { + await this.buildSiteHelper(); + } catch (error) { + await Site.rejectHandler(error, [this.tempPath, this.outputPath]); + } + } + + /** + * Checks the version files of site + subsites and sets them to be ignored in the site config. + */ + ignoreVersionFiles(pathToRootDir) { + const pathToMainVersionFile = path.join(this.rootPath, pathToRootDir, VERSIONS_DATA_NAME); + + // find the versions json file and ignore all archives of the current site + if (fs.pathExistsSync(pathToMainVersionFile)) { + const mainVersionFileJson = fs.readJSONSync(pathToMainVersionFile); + + mainVersionFileJson.versions.forEach((vers) => { + const filePath = pathToRootDir !== '' + ? `${pathToRootDir}/${vers.archivePath}/**` + : `${vers.archivePath}/**`; + this.siteConfig.ignore.push(filePath); + }); + } + + // Do not transfer the versions file into the archived site + // TODO: replace the thing in brackets with rootVersionPath and see if it still works + this.siteConfig.ignore.push(path.posix.join(pathToRootDir, VERSIONS_DATA_NAME)); + + // Find versioned subsites, recursively ignore all version directories inside that + const pathToDirWithVersion = path.join(this.rootPath, pathToRootDir); + + const pathsToVersionFiles + = walkSync(pathToDirWithVersion, { directories: false, ignore: this.siteConfig.ignore }) + .filter(x => x.endsWith(VERSIONS_DATA_NAME)) + // assumes versions files are in _markbind folder + .map(x => path.dirname(path.relative(pathToDirWithVersion, x))) + .map(x => path.dirname(x)); + + pathsToVersionFiles.forEach(p => this.ignoreVersionFiles(p)); + } + + /** + * Reads version data from the version file. + * + * @param {string} versionDataFile, default is VERSIONS_DATA_NAME at the root + * @returns a json object + */ + async readVersionData(versionDataFile = VERSIONS_DATA_NAME) { + const versionsPath = path.join(this.rootPath, versionDataFile); + try { + if (!fs.pathExistsSync(versionsPath)) { + // Initialize the versions.json file. + fs.outputJSONSync(versionsPath, { versions: [] }, { spaces: 2 }); + } + return fs.readJSON(versionsPath); + } catch (error) { + await Site.rejectHandler(error, [this.tempPath, this.outputPath]); + return null; + } + } + + /** + * If the versions.json file exists, update it with a new version. + * Otherwise, create a new versions file to store information about archived versions + * + * @param {boolean} verbose Flag to emit logs of the operation + * @returns Returns the json object of the current versions data. + */ + async writeVersionsFile(versionName, archivePath, verbose = true) { + const newVersionData = { + versionName, + buildVer: MARKBIND_VERSION, + archivePath, + baseUrl: this.siteConfig.baseUrl, + }; + + const versionsJson = await this.readVersionData(VERSIONS_DATA_NAME); + + // Add in or update this new version data in the versions file. + const idx = versionsJson.versions.findIndex(vers => vers.versionName === newVersionData.versionName); + if (idx === -1) { + versionsJson.versions.push(newVersionData); + } else { + if (versionsJson.versions[idx].archivePath !== newVersionData.archivePath) { + throw Error('The version name is the same as a previously archived version, but the' + + ' archive path is not. This is likely to be an error as the previous version will' + + ' no longer be tracked and managed. Please choose a different name or manually' + + ' change the clashing name in the versions.json file to a different name'); + } + versionsJson.versions[idx] = newVersionData; + } + fs.writeJsonSync(VERSIONS_DATA_NAME, versionsJson, { spaces: 2 }); + if (verbose) { + logger.info(`versions.json file updated for version '${versionName}'`); + } + + return versionsJson; + } + + /** + * Copies over all versioned files from a given folder to be deployed. + * @param {Array} versionFolders is the directory the versions are within + */ + async copyVersions(versionFolders) { + const versionFoldersArray = versionFolders.map(f => f.archivePath); + try { + versionFoldersArray.map(async (versionFolder) => { + fs.copy(path.format(path.parse(versionFolder)), path.join(SITE_FOLDER_NAME, versionFolder)); + }); + await Promise.all(versionFoldersArray); + logger.info('Versioned site(s) copied'); + } catch (error) { + await Site.rejectHandler(error, [this.tempPath, this.outputPath]); + } + } + deploy(ciTokenVar) { const defaultDeployConfig = { branch: 'gh-pages', diff --git a/packages/core/test/unit/Site.test.js b/packages/core/test/unit/Site.test.js index 62a91205c0..2d15417793 100644 --- a/packages/core/test/unit/Site.test.js +++ b/packages/core/test/unit/Site.test.js @@ -7,9 +7,12 @@ const { INDEX_MD_DEFAULT, PAGE_NJK, SITE_JSON_DEFAULT, + VERSIONS_DEFAULT, getDefaultTemplateFileFullPath, } = require('./utils/data'); +const MARKBIND_VERSION = require('../../package.json').version; + const DEFAULT_TEMPLATE = 'default'; jest.mock('fs'); @@ -101,7 +104,7 @@ test('Site read site config for default', async () => { }; fs.vol.fromJSON(json, ''); - const expectedSiteConfigDefaults = { enableSearch: true }; + const expectedSiteConfigDefaults = { enableSearch: true, versions: [] }; const expectedSiteConfig = { ...JSON.parse(SITE_JSON_DEFAULT), ...expectedSiteConfigDefaults }; const site = new Site('./', '_site'); const siteConfig = await site.readSiteConfig(); @@ -113,6 +116,7 @@ test('Site read site config for default', async () => { expect(siteConfig.pages).toEqual(expectedSiteConfig.pages); expect(siteConfig.deploy).toEqual(expectedSiteConfig.deploy); expect(siteConfig.enableSearch).toEqual(expectedSiteConfig.enableSearch); + expect(siteConfig.versions).toEqual(expectedSiteConfig.versions); }); test('Site read site config for custom site config', async () => { @@ -133,6 +137,7 @@ test('Site read site config for custom site config', async () => { message: 'Site Update.', }, enableSearch: true, + versions: ['v1', 'v2'], }; const json = { ...PAGE_NJK, @@ -148,6 +153,7 @@ test('Site read site config for custom site config', async () => { expect(siteConfig.ignore).toEqual(customSiteJson.ignore); expect(siteConfig.deploy).toEqual(customSiteJson.deploy); expect(siteConfig.enableSearch).toEqual(customSiteJson.enableSearch); + expect(siteConfig.versions).toEqual(customSiteJson.versions); }); test('Site resolves variables referencing other variables', async () => { @@ -676,3 +682,212 @@ siteJsonPageExclusionTestCases.forEach((testCase) => { .toEqual(testCase.expected); }); }); + +test('Site reads correct versions from versions file', async () => { + const json = { + ...PAGE_NJK, + 'site.json': SITE_JSON_DEFAULT, + '_markbind/versions.json': VERSIONS_DEFAULT, + }; + fs.vol.fromJSON(json, ''); + + const site = new Site('./', '_site'); + const someVersionsData = await site.readVersionData(); + expect(someVersionsData).toEqual(JSON.parse(VERSIONS_DEFAULT)); +}); + +test('Site correctly updates the versions in the versions file for an added version', async () => { + const json = { + ...PAGE_NJK, + 'site.json': SITE_JSON_DEFAULT, + '_markbind/versions.json': VERSIONS_DEFAULT, + }; + fs.vol.fromJSON(json, ''); + + const site = new Site('./', '_site'); + await site.readSiteConfig(); + const someVersionsData = await site.writeVersionsFile('newVersionName', 'custom/archive/path'); + const newVersionData = { + versionName: 'newVersionName', + buildVer: MARKBIND_VERSION, + archivePath: 'custom/archive/path', + baseUrl: JSON.parse(SITE_JSON_DEFAULT).baseUrl, + }; + const expectedVersionsFile = JSON.parse(VERSIONS_DEFAULT); + expectedVersionsFile.versions.push(newVersionData); + + expect(someVersionsData).toEqual(expectedVersionsFile); +}); + +test('Site throws an error when user may be wrongly overwriting versions', async () => { + const json = { + ...PAGE_NJK, + 'site.json': SITE_JSON_DEFAULT, + '_markbind/versions.json': VERSIONS_DEFAULT, + }; + fs.vol.fromJSON(json, ''); + + const site = new Site('./', '_site'); + await site.readSiteConfig(); + + expect.assertions(1); + try { + await site.writeVersionsFile('v2', 'custom/archive/path'); + } catch (e) { + expect(e).toEqual(new Error('The version name is the same as a previously archived version, but the' + + ' archive path is not. This is likely to be an error as the previous version will' + + ' no longer be tracked and managed. Please choose a different name or manually' + + ' change the clashing name in the versions.json file to a different name')); + } +}); + +test('Site correctly updates the versions in the versions file for an overwritten version', async () => { + const json = { + ...PAGE_NJK, + 'site.json': SITE_JSON_DEFAULT, + '_markbind/versions.json': VERSIONS_DEFAULT, + }; + fs.vol.fromJSON(json, ''); + + const site = new Site('./', '_site'); + await site.readSiteConfig(); + const someVersionsData = await site.writeVersionsFile( + 'testOverwritingVersion', 'version/testOverwritingVersion'); + const newVersionData = { + versionName: 'testOverwritingVersion', + buildVer: MARKBIND_VERSION, + archivePath: 'version/testOverwritingVersion', + baseUrl: JSON.parse(SITE_JSON_DEFAULT).baseUrl, + }; + + const expectedVersionsData = JSON.parse(VERSIONS_DEFAULT); + expectedVersionsData.versions[2] = newVersionData; // because constant is hardcoded + + expect(someVersionsData).toEqual(expectedVersionsData); +}); + +const copyingArchivedSiteTestCases = [ + { + name: 'No versions copied when none are specified in site.json', + versionsToGenerate: false, + versions: [], + expected: + { + v1: false, + v2: false, + differentBaseUrl: false, + }, + }, + { + name: 'Versions specified in site.json are copied over', + versionsToGenerate: false, + versions: ['v2'], + expected: + { + v1: false, + v2: true, + differentBaseUrl: false, + }, + }, + { + name: 'The different versions specified in flag override those in site.json', + versionsToGenerate: ['v1'], + versions: ['v2'], + expected: + { + v1: true, + v2: false, + differentBaseUrl: false, + }, + }, + { + name: 'Versions flag overrides versions specified in site.json', + versionsToGenerate: true, + versions: ['v2'], + expected: + { + v1: false, + v2: false, + differentBaseUrl: false, + }, + }, + { + name: 'Even when specified, the version with a differentBaseUrl is not deployed', + versionsToGenerate: false, + versions: ['differentBaseUrl'], + expected: + { + v1: false, + v2: false, + differentBaseUrl: false, + }, + }, +]; + +// TODO: +copyingArchivedSiteTestCases.forEach((testCase) => { + test(testCase.name, async () => { + const originalCopy = fs.copy; + fs.copy = jest.fn(); + const json = { + ...PAGE_NJK, + '_markbind/versions.json': VERSIONS_DEFAULT, + _site: {}, + 'version/v1/index1.html': '', + 'version/v2/index2.html': '', + 'version/differentBaseUrl/neverCopied.html': '', + }; + fs.vol.fromJSON(json, ''); + + const site = new Site('./', '_site'); + site.siteConfig = { baseUrl: '' }; + site.siteConfig.versions = testCase.versions; + + await site.copySpecifiedVersions(testCase.versionsToGenerate); + + let timesCalled = 0; + + if (testCase.expected.v1) { + expect(fs.copy).toBeCalledWith(path.join('version', 'v1'), path.join('_site', 'version', 'v1')); + timesCalled += 1; + } + if (testCase.expected.v2) { + expect(fs.copy).toBeCalledWith(path.join('version', 'v2'), path.join('_site', 'version', 'v2')); + timesCalled += 1; + } + if (testCase.expected.differentBaseUrl) { + expect(fs.copy).toBeCalledWith(path.join('version', 'differentBaseUrl'), + path.join('_site', 'version', 'differentBaseUrl')); + timesCalled += 1; + } + expect(fs.copy).toBeCalledTimes(timesCalled); + + fs.copy = originalCopy; + }); +}); + +test('Site ignores previously archived versions when archiving', async () => { + const json = { + ...PAGE_NJK, + '_markbind/versions.json': VERSIONS_DEFAULT, + 'subsite/_markbind/versions.json': VERSIONS_DEFAULT, + }; + fs.vol.fromJSON(json, ''); + const expectedIgnoredFiles = [ + 'version/v1/**', + 'version/v2/**', + 'version/testOverwritingVersion/**', + 'version/differentBaseUrl/**', + '_markbind/versions.json', + 'subsite/version/v1/**', + 'subsite/version/v2/**', + 'subsite/version/testOverwritingVersion/**', + 'subsite/version/differentBaseUrl/**', + 'subsite/_markbind/versions.json', + ]; + const site = new Site('./', '_site'); + site.siteConfig = { baseUrl: '', versions: ['v1'], ignore: [] }; + + await site.ignoreVersionFiles(''); + expect(site.siteConfig.ignore).toEqual(expectedIgnoredFiles); +}); diff --git a/packages/core/test/unit/utils/data.js b/packages/core/test/unit/utils/data.js index b41fe1dd2f..a6199c59f0 100644 --- a/packages/core/test/unit/utils/data.js +++ b/packages/core/test/unit/utils/data.js @@ -1,4 +1,5 @@ const path = require('path'); +const MARKBIND_VERSION = require('../../../package.json').version; const PAGE_NJK = ` @@ -85,6 +86,35 @@ module.exports.INDEX_MD_DEFAULT = '\n' + '# Hello world\n' + 'Welcome to your page generated with MarkBind.\n'; +module.exports.VERSIONS_DEFAULT = '{\n' + + ' "versions": [\n' + + ' {\n' + + ' "versionName": "v1",\n' + + ` "buildVer": "${MARKBIND_VERSION}",\n` + + ' "archivePath": "version/v1",\n' + + ' "baseUrl": ""\n' + + ' },\n' + + ' {\n' + + ' "versionName": "v2",\n' + + ` "buildVer": "${MARKBIND_VERSION}",\n` + + ' "archivePath": "version/v2",\n' + + ' "baseUrl": ""\n' + + ' },\n' + + ' {\n' + + ' "versionName": "testOverwritingVersion",\n' + + ' "buildVer": "shouldBeOverwritten",\n' + + ' "archivePath": "version/testOverwritingVersion",\n' + + ' "baseUrl": ""\n' + + ' },\n' + + ' {\n' + + ' "versionName": "differentBaseUrl",\n' + + ` "buildVer": "${MARKBIND_VERSION}",\n` + + ' "archivePath": "version/differentBaseUrl",\n' + + ' "baseUrl": "/markbind"\n' + + ' }\n' + + ' ]\n' + + '}\n'; + const DEFAULT_TEMPLATE_DIRECTORY = path.join(__dirname, '../../../template/default'); function getDefaultTemplateFileFullPath(relativePath) {