End-to-end testing, CI script, and JavaScript QoL improvements:
- **Environment variable formatting** - Updated environment variable configuration from array format to object format to comply with Lefthook schema validation requirements. - **Unified link testing** - Consolidated multiple product-specific link testing commands into a single `e2e-links` command that processes all staged Markdown and HTML files across content directories. - **Package script integration** - Modified commands to use centralized yarn scripts instead of direct execution, improving maintainability and consistency. - **Source information extraction** - Enhanced to correctly extract and report source information from frontmatter. - **URL and source mapping** - Improved handling of URL to source path mapping for better reporting. - **Ignored anchor links configuration** - Added proper exclusion of behavior-triggering anchor links (like tab navigation) to prevent false positives. - **Request options correction** - Fixed Cypress request options to ensure `failOnStatusCode` is properly set when `retryOnStatusCodeFailure` is enabled. - **Improved error reporting** - Enhanced error reporting with more context about broken links. - **New test scripts added** - Added centralized testing scripts for link checking and codeblock validation. - **Product-specific test commands** - Added commands for each product version (InfluxDB v2, v3 Core, Enterprise, Cloud, etc.). - **API docs testing** - Added specialized commands for testing API documentation links. - **Comprehensive test runners** - Added commands to run all tests of a specific type (`test:links:all`, `test:codeblocks:all`). - Fix Docker build command and update CONTRIBUTING. chore(js): JavaScript QoL improvements: - Refactor main.js with a componentRegistry object and clear initialization of components and globals - Add a standard index.js with all necessary exports. - Update javascript.html to use the index.js - Remove jQuery script tag from header javascript.html (remains in footer) - Update package file to improve module discovery. - Improve Hugo and ESLint config for module discovery and ES6 syntaxpull/6075/head
parent
853e628880
commit
4cfff239f3
|
@ -16,6 +16,8 @@ node_modules
|
||||||
!telegraf-build/scripts
|
!telegraf-build/scripts
|
||||||
!telegraf-build/README.md
|
!telegraf-build/README.md
|
||||||
/cypress/screenshots/*
|
/cypress/screenshots/*
|
||||||
|
/cypress/videos/*
|
||||||
|
test-results.xml
|
||||||
/influxdb3cli-build-scripts/content
|
/influxdb3cli-build-scripts/content
|
||||||
.vscode/*
|
.vscode/*
|
||||||
.idea
|
.idea
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$LEFTHOOK" = "0" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
call_lefthook()
|
||||||
|
{
|
||||||
|
if test -n "$LEFTHOOK_BIN"
|
||||||
|
then
|
||||||
|
"$LEFTHOOK_BIN" "$@"
|
||||||
|
elif lefthook -h >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
lefthook "$@"
|
||||||
|
else
|
||||||
|
dir="$(git rev-parse --show-toplevel)"
|
||||||
|
osArch=$(uname | tr '[:upper:]' '[:lower:]')
|
||||||
|
cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/')
|
||||||
|
if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook"
|
||||||
|
then
|
||||||
|
"$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@"
|
||||||
|
elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook"
|
||||||
|
then
|
||||||
|
"$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@"
|
||||||
|
elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook"
|
||||||
|
then
|
||||||
|
"$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@"
|
||||||
|
elif test -f "$dir/node_modules/lefthook/bin/index.js"
|
||||||
|
then
|
||||||
|
"$dir/node_modules/lefthook/bin/index.js" "$@"
|
||||||
|
|
||||||
|
elif bundle exec lefthook -h >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
bundle exec lefthook "$@"
|
||||||
|
elif yarn lefthook -h >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
yarn lefthook "$@"
|
||||||
|
elif pnpm lefthook -h >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
pnpm lefthook "$@"
|
||||||
|
elif swift package plugin lefthook >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
swift package --disable-sandbox plugin lefthook "$@"
|
||||||
|
elif command -v mint >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
mint run csjones/lefthook-plugin "$@"
|
||||||
|
else
|
||||||
|
echo "Can't find lefthook in PATH"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
call_lefthook run "serve" "$@"
|
|
@ -28,8 +28,10 @@ For the linting and tests to run, you need to install Docker and Node.js
|
||||||
dependencies.
|
dependencies.
|
||||||
|
|
||||||
\_**Note:**
|
\_**Note:**
|
||||||
We strongly recommend running linting and tests, but you can skip them
|
The git pre-commit and pre-push hooks are configured to run linting and tests automatically
|
||||||
(and avoid installing dependencies)
|
when you commit or push changes.
|
||||||
|
We strongly recommend letting them run, but you can skip them
|
||||||
|
(and avoid installing related dependencies)
|
||||||
by including the `--no-verify` flag with your commit--for example, enter the following command in your terminal:
|
by including the `--no-verify` flag with your commit--for example, enter the following command in your terminal:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
@ -51,7 +53,7 @@ dev dependencies used in pre-commit hooks for linting, syntax-checking, and test
|
||||||
Dev dependencies include:
|
Dev dependencies include:
|
||||||
|
|
||||||
- [Lefthook](https://github.com/evilmartians/lefthook): configures and
|
- [Lefthook](https://github.com/evilmartians/lefthook): configures and
|
||||||
manages pre-commit hooks for linting and testing Markdown content.
|
manages git pre-commit and pre-push hooks for linting and testing Markdown content.
|
||||||
- [prettier](https://prettier.io/docs/en/): formats code, including Markdown, according to style rules for consistency
|
- [prettier](https://prettier.io/docs/en/): formats code, including Markdown, according to style rules for consistency
|
||||||
- [Cypress]: e2e testing for UI elements and URLs in content
|
- [Cypress]: e2e testing for UI elements and URLs in content
|
||||||
|
|
||||||
|
@ -93,9 +95,11 @@ Make your suggested changes being sure to follow the [style and formatting guide
|
||||||
|
|
||||||
## Lint and test your changes
|
## Lint and test your changes
|
||||||
|
|
||||||
|
`package.json` contains scripts for running tests and linting.
|
||||||
|
|
||||||
### Automatic pre-commit checks
|
### Automatic pre-commit checks
|
||||||
|
|
||||||
docs-v2 uses Lefthook to manage Git hooks, such as pre-commit hooks that lint Markdown and test code blocks.
|
docs-v2 uses Lefthook to manage Git hooks that run during pre-commit and pre-push. The hooks run the scripts defined in `package.json` to lint Markdown and test code blocks.
|
||||||
When you try to commit changes (`git commit`), Git runs
|
When you try to commit changes (`git commit`), Git runs
|
||||||
the commands configured in `lefthook.yml` which pass your **staged** files to Vale,
|
the commands configured in `lefthook.yml` which pass your **staged** files to Vale,
|
||||||
Prettier, Cypress (for UI tests and link-checking), and Pytest (for testing Python and shell code in code blocks).
|
Prettier, Cypress (for UI tests and link-checking), and Pytest (for testing Python and shell code in code blocks).
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './main.js';
|
|
@ -6,9 +6,6 @@
|
||||||
/** Import modules that are not components.
|
/** Import modules that are not components.
|
||||||
* TODO: Refactor these into single-purpose component modules.
|
* TODO: Refactor these into single-purpose component modules.
|
||||||
*/
|
*/
|
||||||
// import * as codeblocksPreferences from './api-libs.js';
|
|
||||||
// import * as datetime from './datetime.js';
|
|
||||||
// import * as featureCallouts from './feature-callouts.js';
|
|
||||||
import * as apiLibs from './api-libs.js';
|
import * as apiLibs from './api-libs.js';
|
||||||
import * as codeControls from './code-controls.js';
|
import * as codeControls from './code-controls.js';
|
||||||
import * as contentInteractions from './content-interactions.js';
|
import * as contentInteractions from './content-interactions.js';
|
||||||
|
@ -21,15 +18,6 @@ import * as pageContext from './page-context.js';
|
||||||
import * as pageFeedback from './page-feedback.js';
|
import * as pageFeedback from './page-feedback.js';
|
||||||
import * as tabbedContent from './tabbed-content.js';
|
import * as tabbedContent from './tabbed-content.js';
|
||||||
import * as v3Wayfinding from './v3-wayfinding.js';
|
import * as v3Wayfinding from './v3-wayfinding.js';
|
||||||
// import * as homeInteractions from './home-interactions.js';
|
|
||||||
// import { getUrls, getReferrerHost, InfluxDBUrl } from './influxdb-url.js';
|
|
||||||
// import * as keybindings from './keybindings.js';
|
|
||||||
// import * as listFilters from './list-filters.js';
|
|
||||||
// import { Modal } from './modal.js';
|
|
||||||
// import { showNotifications } from './notifications.js';
|
|
||||||
// import ReleaseTOC from './release-toc.js';
|
|
||||||
// import * as scroll from './scroll.js';
|
|
||||||
// import { TabbedContent } from './tabbed-content.js';
|
|
||||||
|
|
||||||
/** Import component modules
|
/** Import component modules
|
||||||
* The component pattern organizes JavaScript, CSS, and HTML for a specific UI element or interaction:
|
* The component pattern organizes JavaScript, CSS, and HTML for a specific UI element or interaction:
|
||||||
|
@ -41,40 +29,95 @@ import * as v3Wayfinding from './v3-wayfinding.js';
|
||||||
import AskAITrigger from './ask-ai-trigger.js';
|
import AskAITrigger from './ask-ai-trigger.js';
|
||||||
import CodePlaceholder from './code-placeholders.js';
|
import CodePlaceholder from './code-placeholders.js';
|
||||||
import { CustomTimeTrigger } from './custom-timestamps.js';
|
import { CustomTimeTrigger } from './custom-timestamps.js';
|
||||||
|
import FluxInfluxDBVersionsTrigger from './flux-influxdb-versions.js';
|
||||||
import { SearchButton } from './search-button.js';
|
import { SearchButton } from './search-button.js';
|
||||||
import { SidebarToggle } from './sidebar-toggle.js';
|
import { SidebarToggle } from './sidebar-toggle.js';
|
||||||
import Theme from './theme.js';
|
import Theme from './theme.js';
|
||||||
import ThemeSwitch from './theme-switch.js';
|
import ThemeSwitch from './theme-switch.js';
|
||||||
// import CodeControls from './code-controls.js';
|
|
||||||
// import ContentInteractions from './content-interactions.js';
|
|
||||||
// import CustomTimestamps from './custom-timestamps.js';
|
|
||||||
// import Diagram from './Diagram.js';
|
|
||||||
// import FluxGroupKeysExample from './FluxGroupKeysExample.js';
|
|
||||||
import FluxInfluxDBVersionsTrigger from './flux-influxdb-versions.js';
|
|
||||||
// import PageFeedback from './page-feedback.js';
|
|
||||||
// import SearchInput from './SearchInput.js';
|
|
||||||
// import Sidebar from './Sidebar.js';
|
|
||||||
// import V3Wayfinding from './v3-wayfinding.js';
|
|
||||||
// import VersionSelector from './VersionSelector.js';
|
|
||||||
|
|
||||||
// Expose libraries and components within a namespaced object (for backwards compatibility or testing)
|
/**
|
||||||
// Expose libraries and components within a namespaced object (for backwards compatibility or testing)
|
* Component Registry
|
||||||
|
* A central registry that maps component names to their constructor functions.
|
||||||
|
* Add new components to this registry as they are created or migrated from non-component modules.
|
||||||
|
* This allows for:
|
||||||
|
* 1. Automatic component initialization based on data-component attributes
|
||||||
|
* 2. Centralized component management
|
||||||
|
* 3. Easy addition/removal of components
|
||||||
|
* 4. Simplified testing and debugging
|
||||||
|
*/
|
||||||
|
const componentRegistry = {
|
||||||
|
'ask-ai-trigger': AskAITrigger,
|
||||||
|
'code-placeholder': CodePlaceholder,
|
||||||
|
'custom-time-trigger': CustomTimeTrigger,
|
||||||
|
'flux-influxdb-versions-trigger': FluxInfluxDBVersionsTrigger,
|
||||||
|
'search-button': SearchButton,
|
||||||
|
'sidebar-toggle': SidebarToggle,
|
||||||
|
'theme': Theme,
|
||||||
|
'theme-switch': ThemeSwitch
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize global namespace for documentation JavaScript
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
* Exposes core modules for debugging, testing, and backwards compatibility
|
||||||
|
*/
|
||||||
|
function initGlobals() {
|
||||||
if (typeof window.influxdatadocs === 'undefined') {
|
if (typeof window.influxdatadocs === 'undefined') {
|
||||||
window.influxdatadocs = {};
|
window.influxdatadocs = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose modules to the global object for debugging, testing, and backwards compatibility for non-ES6 modules.
|
// Expose modules to the global object for debugging, testing, and backwards compatibility
|
||||||
window.influxdatadocs.delay = delay;
|
window.influxdatadocs.delay = delay;
|
||||||
window.influxdatadocs.localStorage = window.LocalStorageAPI = localStorage;
|
window.influxdatadocs.localStorage = window.LocalStorageAPI = localStorage;
|
||||||
window.influxdatadocs.pageContext = pageContext;
|
window.influxdatadocs.pageContext = pageContext;
|
||||||
window.influxdatadocs.toggleModal = modals.toggleModal;
|
window.influxdatadocs.toggleModal = modals.toggleModal;
|
||||||
|
window.influxdatadocs.componentRegistry = componentRegistry;
|
||||||
|
|
||||||
// On content loaded, initialize (not-component-ready) UI interaction modules
|
return window.influxdatadocs;
|
||||||
// To differentiate these from component-ready modules, these modules typically export an initialize function that wraps UI interactions and event listeners.
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize components based on data-component attributes
|
||||||
|
* @param {Object} globals - The global influxdatadocs namespace
|
||||||
|
*/
|
||||||
|
function initComponents(globals) {
|
||||||
|
const components = document.querySelectorAll('[data-component]');
|
||||||
|
|
||||||
|
components.forEach((component) => {
|
||||||
|
const componentName = component.getAttribute('data-component');
|
||||||
|
const ComponentConstructor = componentRegistry[componentName];
|
||||||
|
|
||||||
|
if (ComponentConstructor) {
|
||||||
|
// Initialize the component and store its instance in the global namespace
|
||||||
|
try {
|
||||||
|
const instance = ComponentConstructor({ component });
|
||||||
|
globals[componentName] = ComponentConstructor;
|
||||||
|
|
||||||
|
// Optionally store component instances for future reference
|
||||||
|
if (!globals.instances) {
|
||||||
|
globals.instances = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globals.instances[componentName]) {
|
||||||
|
globals.instances[componentName] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
globals.instances[componentName].push({
|
||||||
|
element: component,
|
||||||
|
instance
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error initializing component "${componentName}":`, error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Unknown component: "${componentName}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all non-component modules
|
||||||
|
*/
|
||||||
|
function initModules() {
|
||||||
modals.initialize();
|
modals.initialize();
|
||||||
apiLibs.initialize();
|
apiLibs.initialize();
|
||||||
codeControls.initialize();
|
codeControls.initialize();
|
||||||
|
@ -84,67 +127,24 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
pageFeedback.initialize();
|
pageFeedback.initialize();
|
||||||
tabbedContent.initialize();
|
tabbedContent.initialize();
|
||||||
v3Wayfinding.initialize();
|
v3Wayfinding.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
/** Initialize components
|
/**
|
||||||
Component Structure: Each component is structured as a jQuery anonymous function that listens for the document ready state.
|
* Main initialization function
|
||||||
Initialization in main.js: Each component is called in main.js inside a jQuery document ready function to ensure they are initialized when the document is ready.
|
|
||||||
Note: These components should *not* be called directly in the HTML.
|
|
||||||
*/
|
*/
|
||||||
const components = document.querySelectorAll('[data-component]');
|
function init() {
|
||||||
components.forEach((component) => {
|
// Initialize global namespace and expose core modules
|
||||||
const componentName = component.getAttribute('data-component');
|
const globals = initGlobals();
|
||||||
switch (componentName) {
|
|
||||||
case 'ask-ai-trigger':
|
// Initialize non-component UI modules
|
||||||
AskAITrigger({ component });
|
initModules();
|
||||||
window.influxdatadocs[componentName] = AskAITrigger;
|
|
||||||
break;
|
// Initialize components from registry
|
||||||
case 'code-placeholder':
|
initComponents(globals);
|
||||||
CodePlaceholder({ component });
|
}
|
||||||
window.influxdatadocs[componentName] = CodePlaceholder;
|
|
||||||
break;
|
// Initialize everything when the DOM is ready
|
||||||
case 'custom-time-trigger':
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
CustomTimeTrigger({ component });
|
|
||||||
window.influxdatadocs[componentName] = CustomTimeTrigger;
|
// Export public API
|
||||||
break;
|
export { initGlobals, componentRegistry };
|
||||||
case 'flux-influxdb-versions-trigger':
|
|
||||||
FluxInfluxDBVersionsTrigger({ component });
|
|
||||||
window.influxdatadocs[componentName] = FluxInfluxDBVersionsTrigger;
|
|
||||||
break;
|
|
||||||
case 'search-button':
|
|
||||||
SearchButton({ component });
|
|
||||||
window.influxdatadocs[componentName] = SearchButton;
|
|
||||||
break;
|
|
||||||
case 'sidebar-toggle':
|
|
||||||
SidebarToggle({ component });
|
|
||||||
window.influxdatadocs[componentName] = SidebarToggle;
|
|
||||||
break;
|
|
||||||
case 'theme':
|
|
||||||
Theme({ component });
|
|
||||||
window.influxdatadocs[componentName] = Theme;
|
|
||||||
break;
|
|
||||||
// CodeControls();
|
|
||||||
// ContentInteractions();
|
|
||||||
// CustomTimestamps();
|
|
||||||
// Diagram();
|
|
||||||
// FluxGroupKeysExample();
|
|
||||||
// FluxInfluxDBVersionsModal();
|
|
||||||
// InfluxDBUrl();
|
|
||||||
// Modal();
|
|
||||||
// PageFeedback();
|
|
||||||
// ReleaseTOC();
|
|
||||||
// SearchInput();
|
|
||||||
// showNotifications();
|
|
||||||
// Sidebar();
|
|
||||||
// TabbedContent();
|
|
||||||
// ThemeSwitch({});
|
|
||||||
// V3Wayfinding();
|
|
||||||
// VersionSelector();
|
|
||||||
case 'theme-switch':
|
|
||||||
ThemeSwitch({ component });
|
|
||||||
window.influxdatadocs[componentName] = ThemeSwitch;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn(`Unknown component: ${componentName}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -3,7 +3,8 @@
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"*": [
|
"*": [
|
||||||
"*"
|
"*",
|
||||||
|
"../node_modules/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
const { defineConfig } = require('cypress');
|
import { defineConfig } from 'cypress';
|
||||||
const process = require('process');
|
import { cwd as _cwd } from 'process';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
import {
|
||||||
|
BROKEN_LINKS_FILE,
|
||||||
|
initializeReport,
|
||||||
|
readBrokenLinksReport,
|
||||||
|
} from './cypress/support/link-reporter.js';
|
||||||
|
|
||||||
module.exports = defineConfig({
|
export default defineConfig({
|
||||||
e2e: {
|
e2e: {
|
||||||
// Automatically prefix cy.visit() and cy.request() commands with a baseUrl.
|
baseUrl: 'http://localhost:1315',
|
||||||
baseUrl: 'http://localhost:1313',
|
|
||||||
defaultCommandTimeout: 10000,
|
defaultCommandTimeout: 10000,
|
||||||
pageLoadTimeout: 30000,
|
pageLoadTimeout: 30000,
|
||||||
responseTimeout: 30000,
|
responseTimeout: 30000,
|
||||||
|
@ -12,34 +18,128 @@ module.exports = defineConfig({
|
||||||
numTestsKeptInMemory: 5,
|
numTestsKeptInMemory: 5,
|
||||||
projectId: 'influxdata-docs',
|
projectId: 'influxdata-docs',
|
||||||
setupNodeEvents(on, config) {
|
setupNodeEvents(on, config) {
|
||||||
// implement node event listeners here
|
// Browser setup
|
||||||
on('before:browser:launch', (browser, launchOptions) => {
|
on('before:browser:launch', (browser, launchOptions) => {
|
||||||
if (browser.name === 'chrome' && browser.isHeadless) {
|
if (browser.name === 'chrome' && browser.isHeadless) {
|
||||||
// Force Chrome to use a less memory-intensive approach
|
|
||||||
launchOptions.args.push('--disable-dev-shm-usage');
|
launchOptions.args.push('--disable-dev-shm-usage');
|
||||||
launchOptions.args.push('--disable-gpu');
|
launchOptions.args.push('--disable-gpu');
|
||||||
launchOptions.args.push('--disable-extensions');
|
launchOptions.args.push('--disable-extensions');
|
||||||
return launchOptions;
|
return launchOptions;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
on('task', {
|
on('task', {
|
||||||
// Fetch the product list configured in /data/products.yml
|
// Fetch the product list configured in /data/products.yml
|
||||||
getData(filename) {
|
getData(filename) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const yq = require('js-yaml');
|
const cwd = _cwd();
|
||||||
const fs = require('fs');
|
|
||||||
const cwd = process.cwd();
|
|
||||||
try {
|
try {
|
||||||
resolve(
|
resolve(
|
||||||
yq.load(fs.readFileSync(`${cwd}/data/${filename}.yml`, 'utf8'))
|
yaml.load(
|
||||||
|
fs.readFileSync(`${cwd}/data/${filename}.yml`, 'utf8')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
|
||||||
return config;
|
// Log task for reporting
|
||||||
|
log(message) {
|
||||||
|
if (typeof message === 'object') {
|
||||||
|
if (message.type === 'error') {
|
||||||
|
console.error(`\x1b[31m${message.message}\x1b[0m`); // Red
|
||||||
|
} else if (message.type === 'warning') {
|
||||||
|
console.warn(`\x1b[33m${message.message}\x1b[0m`); // Yellow
|
||||||
|
} else if (message.type === 'success') {
|
||||||
|
console.log(`\x1b[32m${message.message}\x1b[0m`); // Green
|
||||||
|
} else if (message.type === 'divider') {
|
||||||
|
console.log(`\x1b[90m${message.message}\x1b[0m`); // Gray
|
||||||
|
} else {
|
||||||
|
console.log(message.message || message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// File tasks
|
||||||
|
writeFile({ path, content }) {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(path, content);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error writing to file ${path}: ${error.message}`);
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
readFile(path) {
|
||||||
|
try {
|
||||||
|
return fs.existsSync(path) ? fs.readFileSync(path, 'utf8') : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reading file ${path}: ${error.message}`);
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Broken links reporting tasks
|
||||||
|
initializeBrokenLinksReport() {
|
||||||
|
return initializeReport();
|
||||||
|
},
|
||||||
|
|
||||||
|
reportBrokenLink(linkData) {
|
||||||
|
try {
|
||||||
|
// Read current report
|
||||||
|
const report = readBrokenLinksReport();
|
||||||
|
|
||||||
|
// Find or create entry for this page
|
||||||
|
let pageReport = report.find((r) => r.page === linkData.page);
|
||||||
|
if (!pageReport) {
|
||||||
|
pageReport = { page: linkData.page, links: [] };
|
||||||
|
report.push(pageReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the broken link to the page's report
|
||||||
|
pageReport.links.push({
|
||||||
|
url: linkData.url,
|
||||||
|
status: linkData.status,
|
||||||
|
type: linkData.type,
|
||||||
|
linkText: linkData.linkText,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write updated report back to file
|
||||||
|
fs.writeFileSync(
|
||||||
|
BROKEN_LINKS_FILE,
|
||||||
|
JSON.stringify(report, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log the broken link immediately to console
|
||||||
|
console.error(
|
||||||
|
`❌ BROKEN LINK: ${linkData.url} (${linkData.status}) - ${linkData.type} on page ${linkData.page}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reporting broken link: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load plugins file using dynamic import for ESM compatibility
|
||||||
|
return import('./cypress/plugins/index.js').then((module) => {
|
||||||
|
return module.default(on, config);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
|
||||||
|
supportFile: 'cypress/support/e2e.js',
|
||||||
|
viewportWidth: 1280,
|
||||||
|
viewportHeight: 720,
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
test_subjects: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
describe('Article links', () => {
|
describe('Article', () => {
|
||||||
const subjects = Cypress.env('test_subjects').split(',');
|
const subjects = Cypress.env('test_subjects').split(',');
|
||||||
// Always use HEAD for downloads to avoid timeouts
|
// Always use HEAD for downloads to avoid timeouts
|
||||||
const useHeadForDownloads = true;
|
const useHeadForDownloads = true;
|
||||||
|
|
||||||
// Helper function to identify download links - improved
|
// Helper function to identify download links
|
||||||
function isDownloadLink(href) {
|
function isDownloadLink(href) {
|
||||||
// Check for common download file extensions
|
// Check for common download file extensions
|
||||||
const downloadExtensions = [
|
const downloadExtensions = [
|
||||||
|
@ -45,79 +45,118 @@ describe('Article links', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to make appropriate request based on link type
|
// Helper function to make appropriate request based on link type
|
||||||
function testLink(href) {
|
function testLink(href, linkText = '', pageUrl) {
|
||||||
|
// Common request options for both methods
|
||||||
|
const requestOptions = {
|
||||||
|
failOnStatusCode: true,
|
||||||
|
timeout: 15000, // Increased timeout for reliability
|
||||||
|
followRedirect: true, // Explicitly follow redirects
|
||||||
|
retryOnNetworkFailure: true, // Retry on network issues
|
||||||
|
retryOnStatusCodeFailure: true, // Retry on 5xx errors
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleFailedLink(url, status, type, redirectChain = '') {
|
||||||
|
// Report broken link to the task which will handle reporting
|
||||||
|
cy.task('reportBrokenLink', {
|
||||||
|
url: url + redirectChain,
|
||||||
|
status,
|
||||||
|
type,
|
||||||
|
linkText,
|
||||||
|
page: pageUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`BROKEN ${type.toUpperCase()} LINK: ${url} (status: ${status})${redirectChain} on ${pageUrl}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (useHeadForDownloads && isDownloadLink(href)) {
|
if (useHeadForDownloads && isDownloadLink(href)) {
|
||||||
cy.log(`** Testing download link with HEAD: ${href} **`);
|
cy.log(`** Testing download link with HEAD: ${href} **`);
|
||||||
cy.request({
|
cy.request({
|
||||||
method: 'HEAD',
|
method: 'HEAD',
|
||||||
url: href,
|
url: href,
|
||||||
|
...requestOptions,
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
const message = `Link is broken: ${href} (status: ${response.status})`;
|
// Check final status after following any redirects
|
||||||
try {
|
if (response.status >= 400) {
|
||||||
expect(response.status).to.be.lt(400);
|
// Build redirect info string if available
|
||||||
} catch (e) {
|
const redirectInfo =
|
||||||
// Log the broken link with the URL for better visibility in reports
|
response.redirects && response.redirects.length > 0
|
||||||
cy.log(`❌ BROKEN LINK: ${href} (${response.status})`);
|
? ` (redirected to: ${response.redirects.join(' -> ')})`
|
||||||
throw new Error(message);
|
: '';
|
||||||
|
|
||||||
|
handleFailedLink(href, response.status, 'download', redirectInfo);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
cy.log(`** Testing link: ${href} **`);
|
cy.log(`** Testing link: ${href} **`);
|
||||||
|
cy.log(JSON.stringify(requestOptions));
|
||||||
cy.request({
|
cy.request({
|
||||||
url: href,
|
url: href,
|
||||||
failOnStatusCode: false,
|
...requestOptions,
|
||||||
timeout: 10000, // 10 second timeout for regular links
|
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
const message = `Link is broken: ${href} (status: ${response.status})`;
|
// Check final status after following any redirects
|
||||||
try {
|
if (response.status >= 400) {
|
||||||
expect(response.status).to.be.lt(400);
|
// Build redirect info string if available
|
||||||
} catch (e) {
|
const redirectInfo =
|
||||||
// Log the broken link with the URL for better visibility in reports
|
response.redirects && response.redirects.length > 0
|
||||||
cy.log(`❌ BROKEN LINK: ${href} (${response.status})`);
|
? ` (redirected to: ${response.redirects.join(' -> ')})`
|
||||||
throw new Error(message);
|
: '';
|
||||||
|
|
||||||
|
handleFailedLink(href, response.status, 'regular', redirectInfo);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Before all tests, initialize the report
|
||||||
|
before(() => {
|
||||||
|
cy.task('initializeBrokenLinksReport');
|
||||||
|
});
|
||||||
|
|
||||||
subjects.forEach((subject) => {
|
subjects.forEach((subject) => {
|
||||||
it(`contains valid internal links on ${subject}`, function () {
|
it(`${subject} has valid internal links`, function () {
|
||||||
cy.visit(`${subject}`);
|
cy.visit(`${subject}`, { timeout: 20000 });
|
||||||
|
|
||||||
// Test internal links
|
// Test internal links
|
||||||
// 1. Timeout and fail the test if article is not found
|
cy.get('article, .api-content').then(($article) => {
|
||||||
// 2. Check each link.
|
|
||||||
// 3. If no links are found, continue without failing
|
|
||||||
cy.get('article').then(($article) => {
|
|
||||||
// Find links without failing the test if none are found
|
// Find links without failing the test if none are found
|
||||||
const $links = $article.find('a[href^="/"]');
|
const $links = $article.find('a[href^="/"]');
|
||||||
if ($links.length === 0) {
|
if ($links.length === 0) {
|
||||||
cy.log('No internal links found on this page');
|
cy.log('No internal links found on this page');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now test each link
|
||||||
cy.wrap($links).each(($a) => {
|
cy.wrap($links).each(($a) => {
|
||||||
const href = $a.attr('href');
|
const href = $a.attr('href');
|
||||||
testLink(href);
|
const linkText = $a.text().trim();
|
||||||
|
testLink(href, linkText, subject);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`checks anchor links on ${subject} (with warnings for missing targets)`, function () {
|
it(`${subject} has valid anchor links`, function () {
|
||||||
cy.visit(`${subject}`);
|
cy.visit(`${subject}`);
|
||||||
|
|
||||||
// Track missing anchors for summary
|
// Define selectors for anchor links to ignore, such as behavior triggers
|
||||||
const missingAnchors = [];
|
const ignoreLinks = ['.tabs a[href^="#"]', '.code-tabs a[href^="#"]'];
|
||||||
|
|
||||||
// Process anchor links individually
|
const anchorSelector =
|
||||||
cy.get('article').then(($article) => {
|
'a[href^="#"]:not(' + ignoreLinks.join('):not(') + ')';
|
||||||
const $anchorLinks = $article.find('a[href^="#"]');
|
|
||||||
|
cy.get('article, .api-content').then(($article) => {
|
||||||
|
const $anchorLinks = $article.find(anchorSelector);
|
||||||
if ($anchorLinks.length === 0) {
|
if ($anchorLinks.length === 0) {
|
||||||
cy.log('No anchor links found on this page');
|
cy.log('No anchor links found on this page');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cy.wrap($anchorLinks).each(($a) => {
|
cy.wrap($anchorLinks).each(($a) => {
|
||||||
const href = $a.prop('href');
|
const href = $a.prop('href');
|
||||||
|
const linkText = $a.text().trim();
|
||||||
|
|
||||||
if (href && href.length > 1) {
|
if (href && href.length > 1) {
|
||||||
// Skip empty anchors (#)
|
|
||||||
// Get just the fragment part
|
// Get just the fragment part
|
||||||
const url = new URL(href);
|
const url = new URL(href);
|
||||||
const anchorId = url.hash.substring(1); // Remove the # character
|
const anchorId = url.hash.substring(1); // Remove the # character
|
||||||
|
@ -127,48 +166,42 @@ describe('Article links', () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use DOM to check if the element exists, but don't fail if missing
|
// Use DOM to check if the element exists
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
const element = win.document.getElementById(anchorId);
|
const element = win.document.getElementById(anchorId);
|
||||||
if (element) {
|
if (!element) {
|
||||||
cy.log(`✅ Anchor target exists: #${anchorId}`);
|
cy.task('reportBrokenLink', {
|
||||||
} else {
|
url: `#${anchorId}`,
|
||||||
// Just warn about the missing anchor
|
status: 404,
|
||||||
cy.log(`⚠️ WARNING: Missing anchor target: #${anchorId}`);
|
type: 'anchor',
|
||||||
missingAnchors.push(anchorId);
|
linkText,
|
||||||
|
page: subject,
|
||||||
|
});
|
||||||
|
cy.log(`⚠️ Missing anchor target: #${anchorId}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.then(() => {
|
|
||||||
// After checking all anchors, log a summary
|
|
||||||
if (missingAnchors.length > 0) {
|
|
||||||
cy.log(
|
|
||||||
`⚠️ Found ${missingAnchors.length} missing anchor targets: ${missingAnchors.join(', ')}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
cy.log('✅ All anchor targets are valid');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`contains valid external links on ${subject}`, function () {
|
it(`${subject} has valid external links`, function () {
|
||||||
cy.visit(`${subject}`);
|
cy.visit(`${subject}`);
|
||||||
|
|
||||||
// Test external links
|
// Test external links
|
||||||
// 1. Timeout and fail the test if article is not found
|
cy.get('article, .api-content').then(($article) => {
|
||||||
// 2. Check each link.
|
|
||||||
// 3. If no links are found, continue without failing
|
|
||||||
cy.get('article').then(($article) => {
|
|
||||||
// Find links without failing the test if none are found
|
// Find links without failing the test if none are found
|
||||||
const $links = $article.find('a[href^="http"]');
|
const $links = $article.find('a[href^="http"]');
|
||||||
if ($links.length === 0) {
|
if ($links.length === 0) {
|
||||||
cy.log('No external links found on this page');
|
cy.log('No external links found on this page');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cy.debug(`Found ${$links.length} external links`);
|
||||||
cy.wrap($links).each(($a) => {
|
cy.wrap($links).each(($a) => {
|
||||||
const href = $a.attr('href');
|
const href = $a.attr('href');
|
||||||
testLink(href);
|
const linkText = $a.text().trim();
|
||||||
});
|
testLink(href, linkText, subject);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************************
|
||||||
|
// This example plugins/index.js can be used to load plugins
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off loading
|
||||||
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/plugins-guide
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Cypress.PluginConfig}
|
||||||
|
*/
|
||||||
|
export default (on, config) => {
|
||||||
|
// `on` is used to hook into various events Cypress emits
|
||||||
|
// `config` is the resolved Cypress config
|
||||||
|
|
||||||
|
// NOTE: The log task is now defined in cypress.config.js
|
||||||
|
// We don't need to register it here to avoid duplication
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
|
@ -14,4 +14,4 @@
|
||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
// Import commands.js using ES2015 syntax:
|
||||||
import './commands'
|
import './commands';
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import http from 'http';
|
||||||
|
import net from 'net';
|
||||||
|
|
||||||
|
// Hugo server constants
|
||||||
|
export const HUGO_PORT = 1315;
|
||||||
|
export const HUGO_LOG_FILE = '/tmp/hugo_server.log';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a port is already in use
|
||||||
|
* @param {number} port - The port to check
|
||||||
|
* @returns {Promise<boolean>} True if port is in use, false otherwise
|
||||||
|
*/
|
||||||
|
export async function isPortInUse(port) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const tester = net
|
||||||
|
.createServer()
|
||||||
|
.once('error', () => resolve(true))
|
||||||
|
.once('listening', () => {
|
||||||
|
tester.close();
|
||||||
|
resolve(false);
|
||||||
|
})
|
||||||
|
.listen(port, '127.0.0.1');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the Hugo server with the specified options
|
||||||
|
* @param {Object} options - Configuration options for Hugo
|
||||||
|
* @param {string} options.configFile - Path to Hugo config file (e.g., 'hugo.testing.yml')
|
||||||
|
* @param {number} options.port - Port number for Hugo server
|
||||||
|
* @param {boolean} options.buildDrafts - Whether to build draft content
|
||||||
|
* @param {boolean} options.noHTTPCache - Whether to disable HTTP caching
|
||||||
|
* @param {string} options.logFile - Path to write Hugo logs
|
||||||
|
* @returns {Promise<Object>} Child process object
|
||||||
|
*/
|
||||||
|
export async function startHugoServer({
|
||||||
|
configFile = 'hugo.testing.yml',
|
||||||
|
port = HUGO_PORT,
|
||||||
|
buildDrafts = true,
|
||||||
|
noHTTPCache = true,
|
||||||
|
logFile = HUGO_LOG_FILE,
|
||||||
|
} = {}) {
|
||||||
|
console.log(`Starting Hugo server on port ${port}...`);
|
||||||
|
|
||||||
|
// Prepare command arguments
|
||||||
|
const hugoArgs = [
|
||||||
|
'hugo',
|
||||||
|
'server',
|
||||||
|
'--config',
|
||||||
|
configFile,
|
||||||
|
'--port',
|
||||||
|
String(port),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (buildDrafts) {
|
||||||
|
hugoArgs.push('--buildDrafts');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noHTTPCache) {
|
||||||
|
hugoArgs.push('--noHTTPCache');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Use npx to find and execute Hugo, which will work regardless of installation method
|
||||||
|
console.log(`Running Hugo with npx: npx ${hugoArgs.join(' ')}`);
|
||||||
|
const hugoProc = spawn('npx', hugoArgs, {
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
shell: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the process started successfully
|
||||||
|
if (!hugoProc || !hugoProc.pid) {
|
||||||
|
return reject(new Error('Failed to start Hugo server via npx'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up logging
|
||||||
|
if (logFile) {
|
||||||
|
hugoProc.stdout.on('data', (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
fs.appendFileSync(logFile, output);
|
||||||
|
process.stdout.write(`Hugo: ${output}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
hugoProc.stderr.on('data', (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
fs.appendFileSync(logFile, output);
|
||||||
|
process.stderr.write(`Hugo ERROR: ${output}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle process errors
|
||||||
|
hugoProc.on('error', (err) => {
|
||||||
|
console.error(`Error in Hugo server process: ${err}`);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for early exit
|
||||||
|
hugoProc.on('close', (code) => {
|
||||||
|
if (code !== null && code !== 0) {
|
||||||
|
reject(new Error(`Hugo process exited early with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resolve with the process object after a short delay to ensure it's running
|
||||||
|
setTimeout(() => {
|
||||||
|
if (hugoProc.killed) {
|
||||||
|
reject(new Error('Hugo process was killed during startup'));
|
||||||
|
} else {
|
||||||
|
resolve(hugoProc);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error starting Hugo server: ${err.message}`);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the Hugo server to be ready
|
||||||
|
* @param {number} timeoutMs - Timeout in milliseconds
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function waitForHugoReady(timeoutMs = 30000) {
|
||||||
|
console.log(
|
||||||
|
`Waiting for Hugo server to be ready on http://localhost:${HUGO_PORT}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Poll the server
|
||||||
|
function checkServer() {
|
||||||
|
const req = http.get(`http://localhost:${HUGO_PORT}`, (res) => {
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
// If we get a response but not 200, try again after delay
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
if (elapsed > timeoutMs) {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Hugo server responded with status ${res.statusCode} after timeout`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setTimeout(checkServer, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => {
|
||||||
|
// Connection errors are expected while server is starting
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
if (elapsed > timeoutMs) {
|
||||||
|
reject(
|
||||||
|
new Error(`Timed out waiting for Hugo server: ${err.message}`)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Try again after a delay
|
||||||
|
setTimeout(checkServer, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling
|
||||||
|
checkServer();
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
/**
|
||||||
|
* Broken Links Reporter
|
||||||
|
* Handles collecting, storing, and reporting broken links found during tests
|
||||||
|
*/
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
export const BROKEN_LINKS_FILE = '/tmp/broken_links_report.json';
|
||||||
|
const SOURCES_FILE = '/tmp/test_subjects_sources.json';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the broken links report from the file system
|
||||||
|
* @returns {Array} Parsed report data or empty array if file doesn't exist
|
||||||
|
*/
|
||||||
|
export function readBrokenLinksReport() {
|
||||||
|
if (!fs.existsSync(BROKEN_LINKS_FILE)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileContent = fs.readFileSync(BROKEN_LINKS_FILE, 'utf8');
|
||||||
|
return fileContent && fileContent !== '[]' ? JSON.parse(fileContent) : [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error reading broken links report: ${err.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the sources mapping file
|
||||||
|
* @returns {Object} A mapping from URLs to their source files
|
||||||
|
*/
|
||||||
|
function readSourcesMapping() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(SOURCES_FILE)) {
|
||||||
|
const sourcesData = JSON.parse(fs.readFileSync(SOURCES_FILE, 'utf8'));
|
||||||
|
return sourcesData.reduce((acc, item) => {
|
||||||
|
if (item.url && item.source) {
|
||||||
|
acc[item.url] = item.source;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Warning: Could not read sources mapping: ${err.message}`);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats and displays the broken links report to the console
|
||||||
|
* @param {Array} brokenLinksReport - The report data to display
|
||||||
|
* @returns {number} The total number of broken links found
|
||||||
|
*/
|
||||||
|
export function displayBrokenLinksReport(brokenLinksReport = null) {
|
||||||
|
// If no report provided, read from file
|
||||||
|
if (!brokenLinksReport) {
|
||||||
|
brokenLinksReport = readBrokenLinksReport();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!brokenLinksReport || brokenLinksReport.length === 0) {
|
||||||
|
console.log('✅ No broken links detected');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load sources mapping
|
||||||
|
const sourcesMapping = readSourcesMapping();
|
||||||
|
|
||||||
|
// Print a prominent header
|
||||||
|
console.error('\n\n' + '='.repeat(80));
|
||||||
|
console.error(' 🚨 BROKEN LINKS DETECTED 🚨 ');
|
||||||
|
console.error('='.repeat(80));
|
||||||
|
|
||||||
|
let totalBrokenLinks = 0;
|
||||||
|
|
||||||
|
brokenLinksReport.forEach((report) => {
|
||||||
|
console.error(`\n📄 PAGE: ${report.page}`);
|
||||||
|
|
||||||
|
// Add source information if available
|
||||||
|
const source = sourcesMapping[report.page];
|
||||||
|
if (source) {
|
||||||
|
console.error(` PAGE CONTENT SOURCE: ${source}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('-'.repeat(40));
|
||||||
|
|
||||||
|
report.links.forEach((link) => {
|
||||||
|
console.error(`• ${link.url}`);
|
||||||
|
console.error(` - Status: ${link.status}`);
|
||||||
|
console.error(` - Type: ${link.type}`);
|
||||||
|
if (link.linkText) {
|
||||||
|
console.error(
|
||||||
|
` - Link text: "${link.linkText.substring(0, 50)}${link.linkText.length > 50 ? '...' : ''}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.error('');
|
||||||
|
totalBrokenLinks++;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Print a prominent summary footer
|
||||||
|
console.error('='.repeat(80));
|
||||||
|
console.error(`📊 TOTAL BROKEN LINKS FOUND: ${totalBrokenLinks}`);
|
||||||
|
console.error('='.repeat(80) + '\n');
|
||||||
|
|
||||||
|
return totalBrokenLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the broken links report file
|
||||||
|
* @returns {boolean} True if initialization was successful
|
||||||
|
*/
|
||||||
|
export function initializeReport() {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(BROKEN_LINKS_FILE, '[]', 'utf8');
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error initializing broken links report: ${err.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,79 +1,139 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { execSync } from 'child_process';
|
|
||||||
import process from 'process';
|
import process from 'process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
|
||||||
// Get file paths from command line arguments
|
// Get file paths from command line arguments
|
||||||
const filePaths = process.argv.slice(2);
|
const filePaths = process.argv.slice(2).filter((arg) => !arg.startsWith('--'));
|
||||||
|
|
||||||
// Parse options
|
// Parse options
|
||||||
const debugMode = process.argv.includes('--debug');
|
const debugMode = process.argv.includes('--debug'); // deprecated, no longer used
|
||||||
|
const jsonMode = process.argv.includes('--json');
|
||||||
|
|
||||||
// Filter for content files
|
// Separate shared content files and regular content files
|
||||||
const contentFiles = filePaths.filter(file =>
|
const sharedContentFiles = filePaths.filter(
|
||||||
file.startsWith('content/') && (file.endsWith('.md') || file.endsWith('.html'))
|
(file) =>
|
||||||
|
file.startsWith('content/shared/') &&
|
||||||
|
(file.endsWith('.md') || file.endsWith('.html'))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (contentFiles.length === 0) {
|
const regularContentFiles = filePaths.filter(
|
||||||
console.log('No content files to check.');
|
(file) =>
|
||||||
|
file.startsWith('content/') &&
|
||||||
|
!file.startsWith('content/shared/') &&
|
||||||
|
(file.endsWith('.md') || file.endsWith('.html'))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find pages that reference shared content files in their frontmatter
|
||||||
|
function findPagesReferencingSharedContent(sharedFilePath) {
|
||||||
|
try {
|
||||||
|
// Remove the leading "content/" to match how it would appear in frontmatter
|
||||||
|
const relativePath = sharedFilePath.replace(/^content\//, '');
|
||||||
|
|
||||||
|
// Use grep to find files that reference this shared content in frontmatter
|
||||||
|
// Look for source: <path> pattern in YAML frontmatter
|
||||||
|
const grepCmd = `grep -l "source: .*${relativePath}" --include="*.md" --include="*.html" -r content/`;
|
||||||
|
|
||||||
|
// Execute grep command and parse results
|
||||||
|
const result = execSync(grepCmd, { encoding: 'utf8' }).trim();
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.split('\n').filter(Boolean);
|
||||||
|
} catch (error) {
|
||||||
|
// grep returns non-zero exit code when no matches are found
|
||||||
|
if (error.status === 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
`Error finding references to ${sharedFilePath}: ${error.message}`
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract source from frontmatter or use the file path as source
|
||||||
|
* @param {string} filePath - Path to the file
|
||||||
|
* @returns {string} Source path
|
||||||
|
*/
|
||||||
|
function extractSourceFromFile(filePath) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const { data } = matter(fileContent);
|
||||||
|
|
||||||
|
// If source is specified in frontmatter, return it
|
||||||
|
if (data.source) {
|
||||||
|
if (data.source.startsWith('/shared')) {
|
||||||
|
return 'content' + data.source;
|
||||||
|
}
|
||||||
|
return data.source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no source in frontmatter or can't read file, use the file path itself
|
||||||
|
return filePath;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error extracting source from ${filePath}: ${error.message}`);
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process shared content files to find pages that reference them
|
||||||
|
let pagesToTest = [...regularContentFiles];
|
||||||
|
|
||||||
|
if (sharedContentFiles.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`Processing ${sharedContentFiles.length} shared content files...`
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const sharedFile of sharedContentFiles) {
|
||||||
|
const referencingPages = findPagesReferencingSharedContent(sharedFile);
|
||||||
|
|
||||||
|
if (referencingPages.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`Found ${referencingPages.length} pages referencing ${sharedFile}`
|
||||||
|
);
|
||||||
|
// Add referencing pages to the list of pages to test (avoid duplicates)
|
||||||
|
pagesToTest = [...new Set([...pagesToTest, ...referencingPages])];
|
||||||
|
} else {
|
||||||
|
console.log(`No pages found referencing ${sharedFile}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pagesToTest.length === 0) {
|
||||||
|
console.log('No content files to map.');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map file paths to URL paths
|
// Map file paths to URL paths and source information
|
||||||
function mapFilePathToUrl(filePath) {
|
function mapFilePathToUrlAndSource(filePath) {
|
||||||
// Remove content/ prefix
|
// Map to URL
|
||||||
let url = filePath.replace(/^content/, '');
|
let url = filePath.replace(/^content/, '');
|
||||||
|
|
||||||
// Handle _index files (both .html and .md)
|
|
||||||
url = url.replace(/\/_index\.(html|md)$/, '/');
|
url = url.replace(/\/_index\.(html|md)$/, '/');
|
||||||
|
|
||||||
// Handle regular .md files
|
|
||||||
url = url.replace(/\.md$/, '/');
|
url = url.replace(/\.md$/, '/');
|
||||||
|
|
||||||
// Handle regular .html files
|
|
||||||
url = url.replace(/\.html$/, '/');
|
url = url.replace(/\.html$/, '/');
|
||||||
|
|
||||||
// Ensure URL starts with a slash
|
|
||||||
if (!url.startsWith('/')) {
|
if (!url.startsWith('/')) {
|
||||||
url = '/' + url;
|
url = '/' + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
// Extract source
|
||||||
|
const source = extractSourceFromFile(filePath);
|
||||||
|
|
||||||
|
return { url, source };
|
||||||
}
|
}
|
||||||
|
|
||||||
const urls = contentFiles.map(mapFilePathToUrl);
|
const mappedFiles = pagesToTest.map(mapFilePathToUrlAndSource);
|
||||||
const urlList = urls.join(',');
|
|
||||||
|
|
||||||
console.log(`Testing links in URLs: ${urlList}`);
|
if (jsonMode) {
|
||||||
|
console.log(JSON.stringify(mappedFiles, null, 2));
|
||||||
// Create environment object with the cypress_test_subjects variable
|
} else {
|
||||||
const envVars = {
|
// Print URL and source info in a format that's easy to parse
|
||||||
...process.env,
|
mappedFiles.forEach((item) => console.log(`${item.url}|${item.source}`));
|
||||||
cypress_test_subjects: urlList,
|
|
||||||
NODE_OPTIONS: '--max-http-header-size=80000 --max-old-space-size=4096'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run Cypress tests with the mapped URLs
|
|
||||||
try {
|
|
||||||
// Choose run mode based on debug flag
|
|
||||||
if (debugMode) {
|
|
||||||
// For debug mode, set the environment variable and open Cypress
|
|
||||||
// The user will need to manually select the test file
|
|
||||||
console.log('Opening Cypress in debug mode.');
|
|
||||||
console.log('Please select the "article-links.cy.js" test file when Cypress opens.');
|
|
||||||
|
|
||||||
execSync('npx cypress open --e2e', {
|
|
||||||
stdio: 'inherit',
|
|
||||||
env: envVars
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// For normal mode, run the test automatically
|
|
||||||
execSync(`npx cypress run --spec "cypress/e2e/content/article-links.cy.js"`, {
|
|
||||||
stdio: 'inherit',
|
|
||||||
env: envVars
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Link check failed:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
|
@ -1,79 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { execSync } from 'child_process';
|
|
||||||
import process from 'process';
|
|
||||||
|
|
||||||
// Get file paths from command line arguments
|
|
||||||
const filePaths = process.argv.slice(2);
|
|
||||||
|
|
||||||
// Parse options
|
|
||||||
const debugMode = process.argv.includes('--debug');
|
|
||||||
|
|
||||||
// Filter for content files
|
|
||||||
const contentFiles = filePaths.filter(file =>
|
|
||||||
file.startsWith('content/') && (file.endsWith('.md') || file.endsWith('.html'))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (contentFiles.length === 0) {
|
|
||||||
console.log('No content files to check.');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map file paths to URL paths
|
|
||||||
function mapFilePathToUrl(filePath) {
|
|
||||||
// Remove content/ prefix
|
|
||||||
let url = filePath.replace(/^content/, '');
|
|
||||||
|
|
||||||
// Handle _index files (both .html and .md)
|
|
||||||
url = url.replace(/\/_index\.(html|md)$/, '/');
|
|
||||||
|
|
||||||
// Handle regular .md files
|
|
||||||
url = url.replace(/\.md$/, '/');
|
|
||||||
|
|
||||||
// Handle regular .html files
|
|
||||||
url = url.replace(/\.html$/, '/');
|
|
||||||
|
|
||||||
// Ensure URL starts with a slash
|
|
||||||
if (!url.startsWith('/')) {
|
|
||||||
url = '/' + url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
const urls = contentFiles.map(mapFilePathToUrl);
|
|
||||||
const urlList = urls.join(',');
|
|
||||||
|
|
||||||
console.log(`Testing links in URLs: ${urlList}`);
|
|
||||||
|
|
||||||
// Create environment object with the cypress_test_subjects variable
|
|
||||||
const envVars = {
|
|
||||||
...process.env,
|
|
||||||
cypress_test_subjects: urlList,
|
|
||||||
NODE_OPTIONS: '--max-http-header-size=80000 --max-old-space-size=4096'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run Cypress tests with the mapped URLs
|
|
||||||
try {
|
|
||||||
// Choose run mode based on debug flag
|
|
||||||
if (debugMode) {
|
|
||||||
// For debug mode, set the environment variable and open Cypress
|
|
||||||
// The user will need to manually select the test file
|
|
||||||
console.log('Opening Cypress in debug mode.');
|
|
||||||
console.log('Please select the "article-links.cy.js" test file when Cypress opens.');
|
|
||||||
|
|
||||||
execSync('npx cypress open --e2e', {
|
|
||||||
stdio: 'inherit',
|
|
||||||
env: envVars
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// For normal mode, run the test automatically
|
|
||||||
execSync(`npx cypress run --spec "cypress/e2e/content/article-links.cy.js"`, {
|
|
||||||
stdio: 'inherit',
|
|
||||||
env: envVars
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Link check failed');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
|
@ -0,0 +1,412 @@
|
||||||
|
/**
|
||||||
|
* InfluxData Documentation E2E Test Runner
|
||||||
|
*
|
||||||
|
* This script automates running Cypress end-to-end tests for the InfluxData documentation site.
|
||||||
|
* It handles starting a local Hugo server, mapping content files to their URLs, running Cypress tests,
|
||||||
|
* and reporting broken links.
|
||||||
|
*
|
||||||
|
* Usage: node run-e2e-specs.js [file paths...] [--spec test-spec-path]
|
||||||
|
*
|
||||||
|
* Example: node run-e2e-specs.js content/influxdb/v2/write-data.md --spec cypress/e2e/content/article-links.cy.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import process from 'process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import cypress from 'cypress';
|
||||||
|
import net from 'net';
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
import { displayBrokenLinksReport } from './link-reporter.js';
|
||||||
|
import {
|
||||||
|
HUGO_PORT,
|
||||||
|
HUGO_LOG_FILE,
|
||||||
|
startHugoServer,
|
||||||
|
waitForHugoReady,
|
||||||
|
} from './hugo-server.js';
|
||||||
|
|
||||||
|
const MAP_SCRIPT = path.resolve('cypress/support/map-files-to-urls.js');
|
||||||
|
const URLS_FILE = '/tmp/test_subjects.txt';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses command line arguments into file and spec arguments
|
||||||
|
* @param {string[]} argv - Command line arguments (process.argv)
|
||||||
|
* @returns {Object} Object containing fileArgs and specArgs arrays
|
||||||
|
*/
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const fileArgs = [];
|
||||||
|
const specArgs = [];
|
||||||
|
let i = 2; // Start at index 2 to skip 'node' and script name
|
||||||
|
|
||||||
|
while (i < argv.length) {
|
||||||
|
if (argv[i] === '--spec') {
|
||||||
|
i++;
|
||||||
|
if (i < argv.length) {
|
||||||
|
specArgs.push(argv[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileArgs.push(argv[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fileArgs, specArgs };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if port is already in use
|
||||||
|
async function isPortInUse(port) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const tester = net
|
||||||
|
.createServer()
|
||||||
|
.once('error', () => resolve(true))
|
||||||
|
.once('listening', () => {
|
||||||
|
tester.close();
|
||||||
|
resolve(false);
|
||||||
|
})
|
||||||
|
.listen(port, '127.0.0.1');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract source information from frontmatter
|
||||||
|
* @param {string} filePath - Path to the markdown file
|
||||||
|
* @returns {string|null} Source information if present
|
||||||
|
*/
|
||||||
|
function getSourceFromFrontmatter(filePath) {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const { data } = matter(fileContent);
|
||||||
|
return data.source || null;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`Warning: Could not extract frontmatter from ${filePath}: ${err.message}`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures a directory exists, creating it if necessary
|
||||||
|
* Also creates an empty file to ensure the directory is not empty
|
||||||
|
* @param {string} dirPath - The directory path to ensure exists
|
||||||
|
*/
|
||||||
|
function ensureDirectoryExists(dirPath) {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
console.log(`Created directory: ${dirPath}`);
|
||||||
|
|
||||||
|
// Create an empty .gitkeep file to ensure the directory exists and isn't empty
|
||||||
|
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`Warning: Could not create directory ${dirPath}: ${err.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Keep track of processes to cleanly shut down
|
||||||
|
let hugoProc = null;
|
||||||
|
let exitCode = 0;
|
||||||
|
let hugoStarted = false;
|
||||||
|
|
||||||
|
// Add this signal handler to ensure cleanup on unexpected termination
|
||||||
|
const cleanupAndExit = (code = 1) => {
|
||||||
|
console.log(`Performing cleanup before exit with code ${code}...`);
|
||||||
|
if (hugoProc && hugoStarted) {
|
||||||
|
try {
|
||||||
|
hugoProc.kill('SIGKILL'); // Use SIGKILL to ensure immediate termination
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error killing Hugo process: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.exit(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle various termination signals
|
||||||
|
process.on('SIGINT', () => cleanupAndExit(1));
|
||||||
|
process.on('SIGTERM', () => cleanupAndExit(1));
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
console.error(`Uncaught exception: ${err.message}`);
|
||||||
|
cleanupAndExit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { fileArgs, specArgs } = parseArgs(process.argv);
|
||||||
|
if (fileArgs.length === 0) {
|
||||||
|
console.error('No file paths provided.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Map file paths to URLs and write to file
|
||||||
|
const mapProc = spawn('node', [MAP_SCRIPT, ...fileArgs], {
|
||||||
|
stdio: ['ignore', 'pipe', 'inherit'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const mappingOutput = [];
|
||||||
|
mapProc.stdout.on('data', (chunk) => {
|
||||||
|
mappingOutput.push(chunk.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((res) => mapProc.on('close', res));
|
||||||
|
|
||||||
|
// Process the mapping output
|
||||||
|
const urlList = mappingOutput
|
||||||
|
.join('')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => {
|
||||||
|
// Parse the URL|SOURCE format
|
||||||
|
if (line.includes('|')) {
|
||||||
|
const [url, source] = line.split('|');
|
||||||
|
return { url, source };
|
||||||
|
} else if (line.startsWith('/')) {
|
||||||
|
// Handle URLs without source (should not happen with our new code)
|
||||||
|
return { url: line, source: null };
|
||||||
|
} else {
|
||||||
|
// Skip log messages
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean); // Remove null entries
|
||||||
|
|
||||||
|
// Log the URLs and sources we'll be testing
|
||||||
|
console.log(`Found ${urlList.length} URLs to test:`);
|
||||||
|
urlList.forEach(({ url, source }) => {
|
||||||
|
console.log(` URL: ${url}`);
|
||||||
|
console.log(` PAGE CONTENT SOURCE: ${source}`);
|
||||||
|
console.log('---');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (urlList.length === 0) {
|
||||||
|
console.log('No URLs to test.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write just the URLs to the test_subjects file for Cypress
|
||||||
|
fs.writeFileSync(URLS_FILE, urlList.map((item) => item.url).join(','));
|
||||||
|
|
||||||
|
// Add source information to a separate file for reference during reporting
|
||||||
|
fs.writeFileSync(
|
||||||
|
'/tmp/test_subjects_sources.json',
|
||||||
|
JSON.stringify(urlList, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Check if port is in use before starting Hugo
|
||||||
|
const portInUse = await isPortInUse(HUGO_PORT);
|
||||||
|
|
||||||
|
if (portInUse) {
|
||||||
|
console.log(
|
||||||
|
`Port ${HUGO_PORT} is already in use. Checking if Hugo is running...`
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
// Try to connect to verify it's a working server
|
||||||
|
await waitForHugoReady(5000); // Short timeout - if it's running, it should respond quickly
|
||||||
|
console.log(
|
||||||
|
`Hugo server already running on port ${HUGO_PORT}, will use existing instance`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`Port ${HUGO_PORT} is in use but not responding as expected: ${err.message}`
|
||||||
|
);
|
||||||
|
console.error('Please stop any processes using this port and try again.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Start Hugo server using the imported function
|
||||||
|
try {
|
||||||
|
console.log(
|
||||||
|
`Starting Hugo server (logs will be written to ${HUGO_LOG_FILE})...`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create or clear the log file
|
||||||
|
fs.writeFileSync(HUGO_LOG_FILE, '');
|
||||||
|
|
||||||
|
// First, check if Hugo is installed and available
|
||||||
|
try {
|
||||||
|
// Try running a simple Hugo version command to check if Hugo is available
|
||||||
|
const hugoCheck = spawn('hugo', ['version'], { shell: true });
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
hugoCheck.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Hugo check failed with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
hugoCheck.on('error', (err) => reject(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Hugo is available on the system');
|
||||||
|
} catch (checkErr) {
|
||||||
|
console.log(
|
||||||
|
'Hugo not found on PATH, will use project-local Hugo via yarn'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the startHugoServer function from hugo-server.js
|
||||||
|
hugoProc = await startHugoServer({
|
||||||
|
configFile: 'hugo.testing.yml',
|
||||||
|
port: HUGO_PORT,
|
||||||
|
buildDrafts: true,
|
||||||
|
noHTTPCache: true,
|
||||||
|
logFile: HUGO_LOG_FILE,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure hugoProc is a valid process object with kill method
|
||||||
|
if (!hugoProc || typeof hugoProc.kill !== 'function') {
|
||||||
|
throw new Error('Failed to get a valid Hugo process object');
|
||||||
|
}
|
||||||
|
|
||||||
|
hugoStarted = true;
|
||||||
|
console.log(`Started Hugo process with PID: ${hugoProc.pid}`);
|
||||||
|
|
||||||
|
// Wait for Hugo to be ready
|
||||||
|
await waitForHugoReady();
|
||||||
|
console.log(`Hugo server ready on port ${HUGO_PORT}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error starting or waiting for Hugo: ${err.message}`);
|
||||||
|
if (hugoProc && typeof hugoProc.kill === 'function') {
|
||||||
|
hugoProc.kill('SIGTERM');
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Prepare Cypress directories
|
||||||
|
try {
|
||||||
|
const screenshotsDir = path.resolve('cypress/screenshots');
|
||||||
|
const videosDir = path.resolve('cypress/videos');
|
||||||
|
const specScreenshotDir = path.join(screenshotsDir, 'article-links.cy.js');
|
||||||
|
|
||||||
|
// Ensure base directories exist
|
||||||
|
ensureDirectoryExists(screenshotsDir);
|
||||||
|
ensureDirectoryExists(videosDir);
|
||||||
|
|
||||||
|
// Create spec-specific screenshot directory with a placeholder file
|
||||||
|
ensureDirectoryExists(specScreenshotDir);
|
||||||
|
|
||||||
|
// Create a dummy screenshot file to prevent trash errors
|
||||||
|
const dummyScreenshotPath = path.join(specScreenshotDir, 'dummy.png');
|
||||||
|
if (!fs.existsSync(dummyScreenshotPath)) {
|
||||||
|
// Create a minimal valid PNG file (1x1 transparent pixel)
|
||||||
|
const minimalPng = Buffer.from([
|
||||||
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
||||||
|
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||||
|
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00,
|
||||||
|
0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00,
|
||||||
|
0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49,
|
||||||
|
0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
|
||||||
|
]);
|
||||||
|
fs.writeFileSync(dummyScreenshotPath, minimalPng);
|
||||||
|
console.log(`Created dummy screenshot file: ${dummyScreenshotPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Cypress directories prepared successfully');
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`Warning: Error preparing Cypress directories: ${err.message}`
|
||||||
|
);
|
||||||
|
// Continue execution - this is not a fatal error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Run Cypress tests
|
||||||
|
let cypressFailed = false;
|
||||||
|
try {
|
||||||
|
console.log(`Running Cypress tests for ${urlList.length} URLs...`);
|
||||||
|
const cypressOptions = {
|
||||||
|
reporter: 'junit',
|
||||||
|
browser: 'chrome',
|
||||||
|
config: {
|
||||||
|
baseUrl: `http://localhost:${HUGO_PORT}`,
|
||||||
|
video: true,
|
||||||
|
trashAssetsBeforeRuns: false, // Prevent trash errors
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
// Pass URLs as a comma-separated string for backward compatibility
|
||||||
|
test_subjects: urlList.map((item) => item.url).join(','),
|
||||||
|
// Add new structured data with source information
|
||||||
|
test_subjects_data: JSON.stringify(urlList),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (specArgs.length > 0) {
|
||||||
|
console.log(`Using specified test specs: ${specArgs.join(', ')}`);
|
||||||
|
cypressOptions.spec = specArgs.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await cypress.run(cypressOptions);
|
||||||
|
|
||||||
|
// Process broken links report
|
||||||
|
const brokenLinksCount = displayBrokenLinksReport();
|
||||||
|
|
||||||
|
if (
|
||||||
|
(results && results.totalFailed && results.totalFailed > 0) ||
|
||||||
|
brokenLinksCount > 0
|
||||||
|
) {
|
||||||
|
console.error(
|
||||||
|
`⚠️ Tests failed: ${results.totalFailed || 0} test(s) failed, ${brokenLinksCount || 0} broken links found`
|
||||||
|
);
|
||||||
|
cypressFailed = true;
|
||||||
|
exitCode = 1;
|
||||||
|
} else if (results) {
|
||||||
|
console.log('✅ Tests completed successfully');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Cypress execution error: ${err.message}`);
|
||||||
|
console.error(
|
||||||
|
`Check Hugo server logs at ${HUGO_LOG_FILE} for any server issues`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Still try to display broken links report if available
|
||||||
|
displayBrokenLinksReport();
|
||||||
|
|
||||||
|
cypressFailed = true;
|
||||||
|
exitCode = 1;
|
||||||
|
} finally {
|
||||||
|
// Stop Hugo server only if we started it
|
||||||
|
if (hugoProc && hugoStarted && typeof hugoProc.kill === 'function') {
|
||||||
|
console.log(`Stopping Hugo server (fast shutdown: ${cypressFailed})...`);
|
||||||
|
|
||||||
|
if (cypressFailed) {
|
||||||
|
hugoProc.kill('SIGKILL');
|
||||||
|
console.log('Hugo server forcibly terminated');
|
||||||
|
} else {
|
||||||
|
const shutdownTimeout = setTimeout(() => {
|
||||||
|
console.error(
|
||||||
|
'Hugo server did not shut down gracefully, forcing termination'
|
||||||
|
);
|
||||||
|
hugoProc.kill('SIGKILL');
|
||||||
|
process.exit(exitCode);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
hugoProc.kill('SIGTERM');
|
||||||
|
|
||||||
|
hugoProc.on('close', () => {
|
||||||
|
clearTimeout(shutdownTimeout);
|
||||||
|
console.log('Hugo server shut down successfully');
|
||||||
|
process.exit(exitCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return to prevent immediate exit
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (hugoStarted) {
|
||||||
|
console.log('Hugo process was started but is not available for cleanup');
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(`Fatal error: ${err}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
|
@ -1,9 +1,98 @@
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import pluginJs from "@eslint/js";
|
import pluginJs from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import importPlugin from "eslint-plugin-import";
|
||||||
|
import a11yPlugin from "eslint-plugin-jsx-a11y";
|
||||||
|
import prettierConfig from "eslint-config-prettier";
|
||||||
|
|
||||||
/** @type {import('eslint').Linter.Config[]} */
|
/** @type {import('eslint').Linter.Config[]} */
|
||||||
export default [
|
export default [
|
||||||
{languageOptions: { globals: globals.browser }},
|
// Base configurations
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
// Hugo-specific globals
|
||||||
|
hugo: "readonly",
|
||||||
|
params: "readonly",
|
||||||
|
// Common libraries used in docs
|
||||||
|
Alpine: "readonly",
|
||||||
|
CodeMirror: "readonly",
|
||||||
|
d3: "readonly"
|
||||||
|
},
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: "module",
|
||||||
|
}
|
||||||
|
},
|
||||||
pluginJs.configs.recommended,
|
pluginJs.configs.recommended,
|
||||||
|
|
||||||
|
// TypeScript configurations (for .ts files)
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
|
||||||
|
// Import plugin for better import/export handling
|
||||||
|
importPlugin.configs.recommended,
|
||||||
|
|
||||||
|
// Accessibility rules (helpful for docs site)
|
||||||
|
a11yPlugin.configs.recommended,
|
||||||
|
|
||||||
|
// Prettier compatibility
|
||||||
|
prettierConfig,
|
||||||
|
|
||||||
|
// Custom rules for documentation project
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// Documentation projects often need to use console for examples
|
||||||
|
"no-console": "off",
|
||||||
|
|
||||||
|
// Module imports
|
||||||
|
"import/extensions": ["error", "ignorePackages"],
|
||||||
|
"import/no-unresolved": "off", // Hugo handles module resolution differently
|
||||||
|
|
||||||
|
// Code formatting
|
||||||
|
"max-len": ["warn", { "code": 80, "ignoreUrls": true, "ignoreStrings": true }],
|
||||||
|
"quotes": ["error", "single", { "avoidEscape": true }],
|
||||||
|
|
||||||
|
// Documentation-specific
|
||||||
|
"valid-jsdoc": ["warn", {
|
||||||
|
"requireReturn": false,
|
||||||
|
"requireReturnType": false,
|
||||||
|
"requireParamType": false
|
||||||
|
}],
|
||||||
|
|
||||||
|
// Hugo template string linting (custom rule)
|
||||||
|
"no-template-curly-in-string": "off", // Allow ${} in strings for Hugo templates
|
||||||
|
|
||||||
|
// Accessibility
|
||||||
|
"jsx-a11y/anchor-is-valid": "warn",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Configuration for specific file patterns
|
||||||
|
{
|
||||||
|
files: ["**/*.js"],
|
||||||
|
rules: {
|
||||||
|
// Rules specific to JavaScript files
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["assets/js/**/*.js"],
|
||||||
|
rules: {
|
||||||
|
// Rules specific to JavaScript in Hugo assets
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.ts"],
|
||||||
|
rules: {
|
||||||
|
// Rules specific to TypeScript files
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Ignore rules for build files and external dependencies
|
||||||
|
ignores: [
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/public/**",
|
||||||
|
"**/resources/**",
|
||||||
|
"**/.hugo_build.lock"
|
||||||
|
]
|
||||||
|
}
|
||||||
];
|
];
|
|
@ -0,0 +1,21 @@
|
||||||
|
# config.testing.toml
|
||||||
|
# This configuration inherits from the base config and overrides draft settings
|
||||||
|
|
||||||
|
# Import base configuration
|
||||||
|
imports: ["hugo.yml"]
|
||||||
|
|
||||||
|
# Override settings for testing
|
||||||
|
buildFuture: true
|
||||||
|
|
||||||
|
# Configure what content is built in testing env
|
||||||
|
params:
|
||||||
|
environment: testing
|
||||||
|
buildTestContent: true
|
||||||
|
|
||||||
|
# Keep your shared content exclusions
|
||||||
|
ignoreFiles:
|
||||||
|
- "content/shared/.*"
|
||||||
|
|
||||||
|
# Ignore specific warning logs
|
||||||
|
ignoreLogs:
|
||||||
|
- warning-goldmark-raw-html
|
13
hugo.yml
13
hugo.yml
|
@ -54,3 +54,16 @@ outputFormats:
|
||||||
mediaType: application/json
|
mediaType: application/json
|
||||||
baseName: pages
|
baseName: pages
|
||||||
isPlainText: true
|
isPlainText: true
|
||||||
|
|
||||||
|
build:
|
||||||
|
# Ensure Hugo correctly processes JavaScript modules
|
||||||
|
jsConfig:
|
||||||
|
nodeEnv: "development"
|
||||||
|
|
||||||
|
module:
|
||||||
|
mounts:
|
||||||
|
- source: assets
|
||||||
|
target: assets
|
||||||
|
|
||||||
|
- source: node_modules
|
||||||
|
target: assets/node_modules
|
|
@ -1,13 +1,3 @@
|
||||||
<!-- START COMPONENT AND JS BUNDLING REFACTOR
|
|
||||||
Eventually, all site-specific JavaScript and external JS
|
|
||||||
dependencies will be bundled in main.js
|
|
||||||
-->
|
|
||||||
<!-- Legacy: keep jquery here until component refactor is for scripts in footer.bundle.js that still require it. -->
|
|
||||||
{{ $jquery := resources.Get "js/jquery-3.5.0.min.js" }}
|
|
||||||
{{ $headerjs := slice $jquery | resources.Concat "js/header.bundle.js" | resources.Fingerprint }}
|
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ $headerjs.RelPermalink }}"></script>
|
|
||||||
|
|
||||||
<!-- $productPathData here is buggy - it might not return the current page path due to the context in which .RelPermalink is called -->
|
<!-- $productPathData here is buggy - it might not return the current page path due to the context in which .RelPermalink is called -->
|
||||||
{{ $productPathData := findRE "[^/]+.*?" .RelPermalink }}
|
{{ $productPathData := findRE "[^/]+.*?" .RelPermalink }}
|
||||||
{{ $product := index $productPathData 0 }}
|
{{ $product := index $productPathData 0 }}
|
||||||
|
@ -23,7 +13,7 @@
|
||||||
{{ $products := .Site.Data.products }}
|
{{ $products := .Site.Data.products }}
|
||||||
{{ $influxdb_urls := .Site.Data.influxdb_urls }}
|
{{ $influxdb_urls := .Site.Data.influxdb_urls }}
|
||||||
<!-- Build main.js -->
|
<!-- Build main.js -->
|
||||||
{{ with resources.Get "js/main.js" }}
|
{{ with resources.Get "js/index.js" }}
|
||||||
{{ $opts := dict
|
{{ $opts := dict
|
||||||
"minify" hugo.IsProduction
|
"minify" hugo.IsProduction
|
||||||
"sourceMap" (cond hugo.IsProduction "" "external")
|
"sourceMap" (cond hugo.IsProduction "" "external")
|
||||||
|
|
177
lefthook.yml
177
lefthook.yml
|
@ -1,135 +1,86 @@
|
||||||
# Refer for explanation to following link:
|
# Refer for explanation to following link:
|
||||||
# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md
|
# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md
|
||||||
#
|
#
|
||||||
pre-push:
|
|
||||||
commands:
|
|
||||||
packages-audit:
|
|
||||||
tags: frontend security
|
|
||||||
run: yarn audit
|
|
||||||
pre-commit:
|
pre-commit:
|
||||||
parallel: true
|
parallel: true
|
||||||
commands:
|
commands:
|
||||||
|
# Report linting warnings and errors, don't output files to stdout
|
||||||
lint-markdown:
|
lint-markdown:
|
||||||
tags: lint
|
tags: lint
|
||||||
glob: "content/**/*.md"
|
glob: 'content/*.md'
|
||||||
run: |
|
run: |
|
||||||
docker compose run --rm --name remark-lint remark-lint '{staged_files}'
|
docker compose run --rm --name remark-lint remark-lint '{staged_files}'
|
||||||
cloud-lint:
|
cloud-lint:
|
||||||
tags: lint,v2
|
tags: lint,v2
|
||||||
glob: "content/influxdb/cloud/**/*.md"
|
glob: 'content/influxdb/cloud/*.md'
|
||||||
run: '.ci/vale/vale.sh
|
run: '.ci/vale/vale.sh
|
||||||
--config=.vale.ini
|
--config=.vale.ini
|
||||||
--minAlertLevel=error {staged_files}'
|
--minAlertLevel=error {staged_files}'
|
||||||
cloud-dedicated-lint:
|
cloud-dedicated-lint:
|
||||||
tags: lint,v3
|
tags: lint,v3
|
||||||
glob: "content/influxdb/cloud-dedicated/**/*.md"
|
glob: 'content/influxdb/cloud-dedicated/*.md'
|
||||||
run: '.ci/vale/vale.sh
|
run: '.ci/vale/vale.sh
|
||||||
--config=content/influxdb/cloud-dedicated/.vale.ini
|
--config=content/influxdb/cloud-dedicated/.vale.ini
|
||||||
--minAlertLevel=error {staged_files}'
|
--minAlertLevel=error {staged_files}'
|
||||||
cloud-serverless-lint:
|
cloud-serverless-lint:
|
||||||
tags: lint,v3
|
tags: lint,v3
|
||||||
glob: "content/influxdb/cloud-serverless/**/*.md"
|
glob: 'content/influxdb/cloud-serverless/*.md'
|
||||||
run: '.ci/vale/vale.sh
|
run: '.ci/vale/vale.sh
|
||||||
--config=content/influxdb/cloud-serverless/.vale.ini
|
--config=content/influxdb/cloud-serverless/.vale.ini
|
||||||
--minAlertLevel=error {staged_files}'
|
--minAlertLevel=error {staged_files}'
|
||||||
clustered-lint:
|
clustered-lint:
|
||||||
tags: lint,v3
|
tags: lint,v3
|
||||||
glob: "content/influxdb/clustered/**/*.md"
|
glob: 'content/influxdb/clustered/*.md'
|
||||||
run: '.ci/vale/vale.sh
|
run: '.ci/vale/vale.sh
|
||||||
--config=content/influxdb/cloud-serverless/.vale.ini
|
--config=content/influxdb/cloud-serverless/.vale.ini
|
||||||
--minAlertLevel=error {staged_files}'
|
--minAlertLevel=error {staged_files}'
|
||||||
telegraf-lint:
|
telegraf-lint:
|
||||||
tags: lint,clients
|
tags: lint,clients
|
||||||
glob: "content/telegraf/**/*.md"
|
glob: 'content/telegraf/*.md'
|
||||||
run: '.ci/vale/vale.sh
|
run: '.ci/vale/vale.sh
|
||||||
--config=.vale.ini
|
--config=.vale.ini
|
||||||
--minAlertLevel=error {staged_files}'
|
--minAlertLevel=error {staged_files}'
|
||||||
v2-lint:
|
v2-lint:
|
||||||
tags: lint,v2
|
tags: lint,v2
|
||||||
glob: "content/influxdb/v2/**/*.md"
|
glob: 'content/influxdb/v2/*.md'
|
||||||
run: '.ci/vale/vale.sh
|
run: '.ci/vale/vale.sh
|
||||||
--config=content/influxdb/v2/.vale.ini
|
--config=content/influxdb/v2/.vale.ini
|
||||||
--minAlertLevel=error {staged_files}'
|
--minAlertLevel=error {staged_files}'
|
||||||
|
|
||||||
# Link checking for InfluxDB v2
|
|
||||||
v2-links:
|
|
||||||
tags: test,links,v2
|
|
||||||
glob: "content/influxdb/v2/**/*.{md,html}"
|
|
||||||
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
|
|
||||||
|
|
||||||
# Link checking for InfluxDB v3 core
|
|
||||||
v3-core-links:
|
|
||||||
tags: test,links,v3
|
|
||||||
glob: "content/influxdb3/core/**/*.{md,html}"
|
|
||||||
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
|
|
||||||
|
|
||||||
# Link checking for InfluxDB v3 enterprise
|
|
||||||
v3-enterprise-links:
|
|
||||||
tags: test,links,v3
|
|
||||||
glob: "content/influxdb3/enterprise/**/*.{md,html}"
|
|
||||||
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
|
|
||||||
|
|
||||||
# Link checking for Cloud products
|
|
||||||
cloud-links:
|
|
||||||
tags: test,links,cloud
|
|
||||||
glob: "content/influxdb/{cloud,cloud-dedicated,cloud-serverless}/**/*.{md,html}"
|
|
||||||
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
|
|
||||||
|
|
||||||
# Link checking for Telegraf
|
|
||||||
telegraf-links:
|
|
||||||
tags: test,links
|
|
||||||
glob: "content/telegraf/**/*.{md,html}"
|
|
||||||
run: 'node cypress/support/map-files-to-urls.mjs {staged_files}'
|
|
||||||
|
|
||||||
cloud-pytest:
|
|
||||||
glob: content/influxdb/cloud/**/*.md
|
|
||||||
tags: test,codeblocks,v2
|
|
||||||
env:
|
|
||||||
- SERVICE: cloud-pytest
|
|
||||||
run: docker compose run --rm --name $SERVICE $SERVICE '{staged_files}'
|
|
||||||
cloud-dedicated-pytest:
|
|
||||||
tags: test,codeblocks,v3
|
|
||||||
glob: content/influxdb/cloud-dedicated/**/*.md
|
|
||||||
env:
|
|
||||||
- SERVICE: cloud-dedicated-pytest
|
|
||||||
run: |
|
|
||||||
./test/scripts/monitor-tests.sh start $SERVICE ;
|
|
||||||
docker compose run --name $SERVICE $SERVICE {staged_files} ;
|
|
||||||
./test/scripts/monitor-tests.sh stop $SERVICE
|
|
||||||
cloud-serverless-pytest:
|
|
||||||
tags: test,codeblocks,v3
|
|
||||||
glob: content/influxdb/cloud-serverless/**/*.md
|
|
||||||
env:
|
|
||||||
- SERVICE: cloud-serverless-pytest
|
|
||||||
run: docker compose run --rm --name $SERVICE $SERVICE '{staged_files}'
|
|
||||||
clustered-pytest:
|
|
||||||
tags: test,codeblocks,v3
|
|
||||||
glob: content/influxdb/clustered/**/*.md
|
|
||||||
env:
|
|
||||||
- SERVICE: clustered-pytest
|
|
||||||
run: |
|
|
||||||
./test/scripts/monitor-tests.sh start $SERVICE ;
|
|
||||||
docker compose run --name $SERVICE $SERVICE {staged_files} ;
|
|
||||||
./test/scripts/monitor-tests.sh stop $SERVICE
|
|
||||||
telegraf-pytest:
|
|
||||||
tags: test,codeblocks
|
|
||||||
glob: content/telegraf/**/*.md
|
|
||||||
env:
|
|
||||||
- SERVICE: telegraf-pytest
|
|
||||||
run: docker compose run --rm --name $SERVICE $SERVICE '{staged_files}'
|
|
||||||
v2-pytest:
|
|
||||||
tags: test,codeblocks,v2
|
|
||||||
glob: content/influxdb/v2/**/*.md
|
|
||||||
env:
|
|
||||||
- SERVICE: v2-pytest
|
|
||||||
run: docker compose run --rm --name $SERVICE $SERVICE '{staged_files}'
|
|
||||||
prettier:
|
prettier:
|
||||||
tags: frontend,style
|
tags: [frontend, style]
|
||||||
glob: "*.{css,js,ts,jsx,tsx}"
|
glob: '*.{css,js,ts,jsx,tsx}'
|
||||||
run: yarn prettier {staged_files}
|
run: |
|
||||||
|
yarn prettier --write --loglevel silent "{staged_files}" > /dev/null 2>&1 ||
|
||||||
build:
|
{ echo "⚠️ Prettier found formatting issues. Automatic formatting applied."
|
||||||
|
git add {staged_files}
|
||||||
|
}
|
||||||
|
pre-push:
|
||||||
commands:
|
commands:
|
||||||
|
packages-audit:
|
||||||
|
tags: frontend security
|
||||||
|
run: yarn audit
|
||||||
|
|
||||||
|
e2e-shortcode-examples:
|
||||||
|
tags: [frontend, test]
|
||||||
|
glob:
|
||||||
|
- assets/*.{js,mjs,css,scss}
|
||||||
|
- layouts/*.html
|
||||||
|
- content/example.md
|
||||||
|
files: /bin/ls content/example.md
|
||||||
|
run: |
|
||||||
|
node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/article-links.cy.js" {files}
|
||||||
|
exit $?
|
||||||
|
|
||||||
|
e2e-links:
|
||||||
|
tags: test,links
|
||||||
|
glob: 'content/**/*.{md,html}'
|
||||||
|
run: |
|
||||||
|
echo "Running link checker for: {staged_files}"
|
||||||
|
yarn test:links {staged_files}
|
||||||
|
exit $?
|
||||||
|
|
||||||
|
# Manage Docker containers
|
||||||
prune-legacy-containers:
|
prune-legacy-containers:
|
||||||
priority: 1
|
priority: 1
|
||||||
tags: test
|
tags: test
|
||||||
|
@ -137,6 +88,48 @@ build:
|
||||||
--filter label=tag=influxdata-docs
|
--filter label=tag=influxdata-docs
|
||||||
--filter status=exited | xargs docker rm)
|
--filter status=exited | xargs docker rm)
|
||||||
|| true'
|
|| true'
|
||||||
rebuild-test-images:
|
build-pytest-image:
|
||||||
tags: test
|
tags: test
|
||||||
run: docker compose build pytest-codeblocks
|
run: yarn build:pytest:image
|
||||||
|
# Test code blocks in markdown files
|
||||||
|
cloud-pytest:
|
||||||
|
glob: content/influxdb/cloud/**/*.md
|
||||||
|
tags: test,codeblocks,v2
|
||||||
|
env:
|
||||||
|
SERVICE: cloud-pytest
|
||||||
|
run: yarn test:codeblocks:cloud '{staged_files}'
|
||||||
|
|
||||||
|
cloud-dedicated-pytest:
|
||||||
|
tags: test,codeblocks,v3
|
||||||
|
glob: content/influxdb/cloud-dedicated/**/*.md
|
||||||
|
run: |
|
||||||
|
yarn test:codeblocks:cloud-dedicated '{staged_files}' &&
|
||||||
|
./test/scripts/monitor-tests.sh stop cloud-dedicated-pytest
|
||||||
|
|
||||||
|
cloud-serverless-pytest:
|
||||||
|
tags: test,codeblocks,v3
|
||||||
|
glob: content/influxdb/cloud-serverless/**/*.md
|
||||||
|
env:
|
||||||
|
SERVICE: cloud-serverless-pytest
|
||||||
|
run: yarn test:codeblocks:cloud-serverless '{staged_files}'
|
||||||
|
|
||||||
|
clustered-pytest:
|
||||||
|
tags: test,codeblocks,v3
|
||||||
|
glob: content/influxdb/clustered/**/*.md
|
||||||
|
run: |
|
||||||
|
yarn test:codeblocks:clustered '{staged_files}' &&
|
||||||
|
./test/scripts/monitor-tests.sh stop clustered-pytest
|
||||||
|
|
||||||
|
telegraf-pytest:
|
||||||
|
tags: test,codeblocks
|
||||||
|
glob: content/telegraf/**/*.md
|
||||||
|
env:
|
||||||
|
SERVICE: telegraf-pytest
|
||||||
|
run: yarn test:codeblocks:telegraf '{staged_files}'
|
||||||
|
|
||||||
|
v2-pytest:
|
||||||
|
tags: test,codeblocks,v2
|
||||||
|
glob: content/influxdb/v2/**/*.md
|
||||||
|
env:
|
||||||
|
SERVICE: v2-pytest
|
||||||
|
run: yarn test:codeblocks:v2 '{staged_files}'
|
47
package.json
47
package.json
|
@ -24,6 +24,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.4",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
@ -33,16 +34,48 @@
|
||||||
"vanillajs-datepicker": "^1.3.4"
|
"vanillajs-datepicker": "^1.3.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"e2e:chrome": "npx cypress run --browser chrome",
|
"build:pytest:image":"docker build -t influxdata/docs-pytest:latest -f Dockerfile.pytest .",
|
||||||
"e2e:o": "npx cypress open",
|
|
||||||
"e2e:o:links": "node cypress/support/map-files-to-urls.mjs content/influxdb3/core/get-started/_index.md --debug",
|
|
||||||
"e2e:api-docs": "export cypress_test_subjects=\"http://localhost:1313/influxdb3/core/api/,http://localhost:1313/influxdb3/enterprise/api/,http://localhost:1313/influxdb3/cloud-dedicated/api/,http://localhost:1313/influxdb3/cloud-dedicated/api/v1/,http://localhost:1313/influxdb/cloud-dedicated/api/v1/,http://localhost:1313/influxdb/cloud-dedicated/api/management/,http://localhost:1313/influxdb3/cloud-dedicated/api/management/\"; npx cypress run --spec cypress/e2e/article-links.cy.js",
|
|
||||||
"lint": "LEFTHOOK_EXCLUDE=test lefthook run pre-commit && lefthook run pre-push",
|
"lint": "LEFTHOOK_EXCLUDE=test lefthook run pre-commit && lefthook run pre-push",
|
||||||
"pre-commit": "lefthook run pre-commit",
|
"pre-commit": "lefthook run pre-commit",
|
||||||
"test-content": "docker compose --profile test up"
|
"test": "echo \"Run 'yarn test:e2e', 'yarn test:links', 'yarn test:codeblocks:all' or a specific test command. e2e and links test commands can take a glob of file paths to test. Some commands run automatically during the git pre-commit and pre-push hooks.\" && exit 0",
|
||||||
|
"test:codeblocks": "echo \"Run a specific codeblocks test command\" && exit 0",
|
||||||
|
"test:codeblocks:all": "docker compose --profile test up",
|
||||||
|
"test:codeblocks:cloud": "docker compose run --rm --name cloud-pytest cloud-pytest",
|
||||||
|
"test:codeblocks:cloud-dedicated": "./test/scripts/monitor-tests.sh start cloud-dedicated-pytest && docker compose run --name cloud-dedicated-pytest cloud-dedicated-pytest",
|
||||||
|
"test:codeblocks:cloud-serverless": "docker compose run --rm --name cloud-serverless-pytest cloud-serverless-pytest",
|
||||||
|
"test:codeblocks:clustered": "./test/scripts/monitor-tests.sh start clustered-pytest && docker compose run --name clustered-pytest clustered-pytest",
|
||||||
|
"test:codeblocks:telegraf": "docker compose run --rm --name telegraf-pytest telegraf-pytest",
|
||||||
|
"test:codeblocks:v2": "docker compose run --rm --name v2-pytest v2-pytest",
|
||||||
|
"test:codeblocks:stop-monitors": "./test/scripts/monitor-tests.sh stop cloud-dedicated-pytest && ./test/scripts/monitor-tests.sh stop clustered-pytest",
|
||||||
|
"test:e2e": "node cypress/support/run-e2e-specs.js",
|
||||||
|
"test:links": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\"",
|
||||||
|
"test:links:v1": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/influxdb/{v1,enterprise_influxdb}/**/*.{md,html}",
|
||||||
|
"test:links:v2": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/influxdb/{cloud,v2}/**/*.{md,html}",
|
||||||
|
"test:links:v3": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/influxdb3/**/*.{md,html}",
|
||||||
|
"test:links:chronograf": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/chronograf/**/*.{md,html}",
|
||||||
|
"test:links:kapacitor": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/kapacitor/**/*.{md,html}",
|
||||||
|
"test:links:telegraf": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/telegraf/**/*.{md,html}",
|
||||||
|
"test:links:shared": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/shared/**/*.{md,html}",
|
||||||
|
"test:links:api-docs": "export cypress_base_url=\"http://localhost:1315\" cypress_test_subjects=\"/influxdb3/core/api/,/influxdb3/enterprise/api/,/influxdb3/cloud-dedicated/api/,/influxdb3/cloud-dedicated/api/v1/,/influxdb/cloud-dedicated/api/v1/,/influxdb/cloud-dedicated/api/management/,/influxdb3/cloud-dedicated/api/management/\"; npx cypress run --spec cypress/e2e/content/article-links.cy.js",
|
||||||
|
"test:shortcode-examples": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/example.md"
|
||||||
|
},
|
||||||
|
"main": "assets/js/main.js",
|
||||||
|
"module": "assets/js/main.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./assets/js/main.js",
|
||||||
|
"require": "./assets/js/main.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"browserslist": [
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead",
|
||||||
|
"not IE 11"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
|
||||||
"module": "main.js",
|
|
||||||
"directories": {
|
"directories": {
|
||||||
"test": "test"
|
"test": "test"
|
||||||
},
|
},
|
||||||
|
|
65
yarn.lock
65
yarn.lock
|
@ -625,6 +625,13 @@ arch@^2.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
|
resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
|
||||||
integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
|
integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
|
||||||
|
|
||||||
|
argparse@^1.0.7:
|
||||||
|
version "1.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||||
|
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
|
||||||
|
dependencies:
|
||||||
|
sprintf-js "~1.0.2"
|
||||||
|
|
||||||
argparse@^2.0.1:
|
argparse@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||||
|
@ -1790,6 +1797,11 @@ espree@^10.0.1, espree@^10.3.0:
|
||||||
acorn-jsx "^5.3.2"
|
acorn-jsx "^5.3.2"
|
||||||
eslint-visitor-keys "^4.2.0"
|
eslint-visitor-keys "^4.2.0"
|
||||||
|
|
||||||
|
esprima@^4.0.0:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||||
|
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||||
|
|
||||||
esquery@^1.5.0:
|
esquery@^1.5.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
|
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
|
||||||
|
@ -1846,6 +1858,13 @@ exsolve@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.4.tgz#7de5c75af82ecd15998328fbf5f2295883be3a39"
|
resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.4.tgz#7de5c75af82ecd15998328fbf5f2295883be3a39"
|
||||||
integrity sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==
|
integrity sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==
|
||||||
|
|
||||||
|
extend-shallow@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
|
||||||
|
integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==
|
||||||
|
dependencies:
|
||||||
|
is-extendable "^0.1.0"
|
||||||
|
|
||||||
extend@~3.0.2:
|
extend@~3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||||
|
@ -2218,6 +2237,16 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0,
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||||
|
|
||||||
|
gray-matter@^4.0.3:
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798"
|
||||||
|
integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==
|
||||||
|
dependencies:
|
||||||
|
js-yaml "^3.13.1"
|
||||||
|
kind-of "^6.0.2"
|
||||||
|
section-matter "^1.0.0"
|
||||||
|
strip-bom-string "^1.0.0"
|
||||||
|
|
||||||
hachure-fill@^0.5.2:
|
hachure-fill@^0.5.2:
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/hachure-fill/-/hachure-fill-0.5.2.tgz#d19bc4cc8750a5962b47fb1300557a85fcf934cc"
|
resolved "https://registry.yarnpkg.com/hachure-fill/-/hachure-fill-0.5.2.tgz#d19bc4cc8750a5962b47fb1300557a85fcf934cc"
|
||||||
|
@ -2377,6 +2406,11 @@ is-core-module@^2.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
hasown "^2.0.2"
|
hasown "^2.0.2"
|
||||||
|
|
||||||
|
is-extendable@^0.1.0:
|
||||||
|
version "0.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
|
||||||
|
integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==
|
||||||
|
|
||||||
is-extglob@^2.1.1:
|
is-extglob@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||||
|
@ -2491,6 +2525,14 @@ js-tokens@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||||
|
|
||||||
|
js-yaml@^3.13.1:
|
||||||
|
version "3.14.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
|
||||||
|
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
|
||||||
|
dependencies:
|
||||||
|
argparse "^1.0.7"
|
||||||
|
esprima "^4.0.0"
|
||||||
|
|
||||||
js-yaml@^4.1.0:
|
js-yaml@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||||
|
@ -2576,6 +2618,11 @@ khroma@^2.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.1.0.tgz#45f2ce94ce231a437cf5b63c2e886e6eb42bbbb1"
|
resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.1.0.tgz#45f2ce94ce231a437cf5b63c2e886e6eb42bbbb1"
|
||||||
integrity sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==
|
integrity sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==
|
||||||
|
|
||||||
|
kind-of@^6.0.0, kind-of@^6.0.2:
|
||||||
|
version "6.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
|
||||||
|
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
|
||||||
|
|
||||||
kolorist@^1.8.0:
|
kolorist@^1.8.0:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c"
|
resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c"
|
||||||
|
@ -3576,6 +3623,14 @@ safe-stable-stringify@^2.3.1:
|
||||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
|
section-matter@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167"
|
||||||
|
integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==
|
||||||
|
dependencies:
|
||||||
|
extend-shallow "^2.0.1"
|
||||||
|
kind-of "^6.0.0"
|
||||||
|
|
||||||
seek-bzip@^1.0.5:
|
seek-bzip@^1.0.5:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4"
|
resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4"
|
||||||
|
@ -3723,6 +3778,11 @@ spdx-license-ids@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz#6d6e980c9df2b6fc905343a3b2d702a6239536c3"
|
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz#6d6e980c9df2b6fc905343a3b2d702a6239536c3"
|
||||||
integrity sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==
|
integrity sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==
|
||||||
|
|
||||||
|
sprintf-js@~1.0.2:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||||
|
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
|
||||||
|
|
||||||
sql-formatter@^15.0.2:
|
sql-formatter@^15.0.2:
|
||||||
version "15.4.11"
|
version "15.4.11"
|
||||||
resolved "https://registry.yarnpkg.com/sql-formatter/-/sql-formatter-15.4.11.tgz#10a8205aa82d60507811360d4735e81d4a21c137"
|
resolved "https://registry.yarnpkg.com/sql-formatter/-/sql-formatter-15.4.11.tgz#10a8205aa82d60507811360d4735e81d4a21c137"
|
||||||
|
@ -3814,6 +3874,11 @@ strip-ansi@^7.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex "^6.0.1"
|
ansi-regex "^6.0.1"
|
||||||
|
|
||||||
|
strip-bom-string@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92"
|
||||||
|
integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==
|
||||||
|
|
||||||
strip-dirs@^2.0.0:
|
strip-dirs@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5"
|
resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5"
|
||||||
|
|
Loading…
Reference in New Issue