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/README.md
|
||||
/cypress/screenshots/*
|
||||
/cypress/videos/*
|
||||
test-results.xml
|
||||
/influxdb3cli-build-scripts/content
|
||||
.vscode/*
|
||||
.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.
|
||||
|
||||
\_**Note:**
|
||||
We strongly recommend running linting and tests, but you can skip them
|
||||
(and avoid installing dependencies)
|
||||
The git pre-commit and pre-push hooks are configured to run linting and tests automatically
|
||||
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:
|
||||
|
||||
```sh
|
||||
|
@ -51,7 +53,7 @@ dev dependencies used in pre-commit hooks for linting, syntax-checking, and test
|
|||
Dev dependencies include:
|
||||
|
||||
- [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
|
||||
- [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
|
||||
|
||||
`package.json` contains scripts for running tests and linting.
|
||||
|
||||
### 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
|
||||
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).
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './main.js';
|
|
@ -6,9 +6,6 @@
|
|||
/** Import modules that are not components.
|
||||
* 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 codeControls from './code-controls.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 tabbedContent from './tabbed-content.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
|
||||
* 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 CodePlaceholder from './code-placeholders.js';
|
||||
import { CustomTimeTrigger } from './custom-timestamps.js';
|
||||
import FluxInfluxDBVersionsTrigger from './flux-influxdb-versions.js';
|
||||
import { SearchButton } from './search-button.js';
|
||||
import { SidebarToggle } from './sidebar-toggle.js';
|
||||
import Theme from './theme.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
|
||||
};
|
||||
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
/**
|
||||
* Initialize global namespace for documentation JavaScript
|
||||
* Exposes core modules for debugging, testing, and backwards compatibility
|
||||
*/
|
||||
function initGlobals() {
|
||||
if (typeof window.influxdatadocs === 'undefined') {
|
||||
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.localStorage = window.LocalStorageAPI = localStorage;
|
||||
window.influxdatadocs.pageContext = pageContext;
|
||||
window.influxdatadocs.toggleModal = modals.toggleModal;
|
||||
window.influxdatadocs.componentRegistry = componentRegistry;
|
||||
|
||||
return window.influxdatadocs;
|
||||
}
|
||||
|
||||
// On content loaded, initialize (not-component-ready) UI interaction modules
|
||||
// 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();
|
||||
apiLibs.initialize();
|
||||
codeControls.initialize();
|
||||
|
@ -84,67 +127,24 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
pageFeedback.initialize();
|
||||
tabbedContent.initialize();
|
||||
v3Wayfinding.initialize();
|
||||
}
|
||||
|
||||
/** Initialize components
|
||||
Component Structure: Each component is structured as a jQuery anonymous function that listens for the document ready state.
|
||||
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]');
|
||||
components.forEach((component) => {
|
||||
const componentName = component.getAttribute('data-component');
|
||||
switch (componentName) {
|
||||
case 'ask-ai-trigger':
|
||||
AskAITrigger({ component });
|
||||
window.influxdatadocs[componentName] = AskAITrigger;
|
||||
break;
|
||||
case 'code-placeholder':
|
||||
CodePlaceholder({ component });
|
||||
window.influxdatadocs[componentName] = CodePlaceholder;
|
||||
break;
|
||||
case 'custom-time-trigger':
|
||||
CustomTimeTrigger({ component });
|
||||
window.influxdatadocs[componentName] = CustomTimeTrigger;
|
||||
break;
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Main initialization function
|
||||
*/
|
||||
function init() {
|
||||
// Initialize global namespace and expose core modules
|
||||
const globals = initGlobals();
|
||||
|
||||
// Initialize non-component UI modules
|
||||
initModules();
|
||||
|
||||
// Initialize components from registry
|
||||
initComponents(globals);
|
||||
}
|
||||
|
||||
// Initialize everything when the DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
// Export public API
|
||||
export { initGlobals, componentRegistry };
|
|
@ -3,7 +3,8 @@
|
|||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": [
|
||||
"*"
|
||||
"*",
|
||||
"../node_modules/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
const { defineConfig } = require('cypress');
|
||||
const process = require('process');
|
||||
import { defineConfig } from 'cypress';
|
||||
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: {
|
||||
// Automatically prefix cy.visit() and cy.request() commands with a baseUrl.
|
||||
baseUrl: 'http://localhost:1313',
|
||||
baseUrl: 'http://localhost:1315',
|
||||
defaultCommandTimeout: 10000,
|
||||
pageLoadTimeout: 30000,
|
||||
responseTimeout: 30000,
|
||||
|
@ -12,34 +18,128 @@ module.exports = defineConfig({
|
|||
numTestsKeptInMemory: 5,
|
||||
projectId: 'influxdata-docs',
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
// Browser setup
|
||||
on('before:browser:launch', (browser, launchOptions) => {
|
||||
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-gpu');
|
||||
launchOptions.args.push('--disable-extensions');
|
||||
return launchOptions;
|
||||
}
|
||||
});
|
||||
|
||||
on('task', {
|
||||
// Fetch the product list configured in /data/products.yml
|
||||
getData(filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const yq = require('js-yaml');
|
||||
const fs = require('fs');
|
||||
const cwd = process.cwd();
|
||||
const cwd = _cwd();
|
||||
try {
|
||||
resolve(
|
||||
yq.load(fs.readFileSync(`${cwd}/data/${filename}.yml`, 'utf8'))
|
||||
yaml.load(
|
||||
fs.readFileSync(`${cwd}/data/${filename}.yml`, 'utf8')
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 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);
|
||||
});
|
||||
return 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" />
|
||||
|
||||
describe('Article links', () => {
|
||||
describe('Article', () => {
|
||||
const subjects = Cypress.env('test_subjects').split(',');
|
||||
// Always use HEAD for downloads to avoid timeouts
|
||||
const useHeadForDownloads = true;
|
||||
|
||||
// Helper function to identify download links - improved
|
||||
// Helper function to identify download links
|
||||
function isDownloadLink(href) {
|
||||
// Check for common download file extensions
|
||||
const downloadExtensions = [
|
||||
|
@ -45,130 +45,163 @@ describe('Article links', () => {
|
|||
}
|
||||
|
||||
// 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)) {
|
||||
cy.log(`** Testing download link with HEAD: ${href} **`);
|
||||
cy.request({
|
||||
method: 'HEAD',
|
||||
url: href,
|
||||
...requestOptions,
|
||||
}).then((response) => {
|
||||
const message = `Link is broken: ${href} (status: ${response.status})`;
|
||||
try {
|
||||
expect(response.status).to.be.lt(400);
|
||||
} catch (e) {
|
||||
// Log the broken link with the URL for better visibility in reports
|
||||
cy.log(`❌ BROKEN LINK: ${href} (${response.status})`);
|
||||
throw new Error(message);
|
||||
// Check final status after following any redirects
|
||||
if (response.status >= 400) {
|
||||
// Build redirect info string if available
|
||||
const redirectInfo =
|
||||
response.redirects && response.redirects.length > 0
|
||||
? ` (redirected to: ${response.redirects.join(' -> ')})`
|
||||
: '';
|
||||
|
||||
handleFailedLink(href, response.status, 'download', redirectInfo);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cy.log(`** Testing link: ${href} **`);
|
||||
cy.log(JSON.stringify(requestOptions));
|
||||
cy.request({
|
||||
url: href,
|
||||
failOnStatusCode: false,
|
||||
timeout: 10000, // 10 second timeout for regular links
|
||||
...requestOptions,
|
||||
}).then((response) => {
|
||||
const message = `Link is broken: ${href} (status: ${response.status})`;
|
||||
try {
|
||||
expect(response.status).to.be.lt(400);
|
||||
} catch (e) {
|
||||
// Log the broken link with the URL for better visibility in reports
|
||||
cy.log(`❌ BROKEN LINK: ${href} (${response.status})`);
|
||||
throw new Error(message);
|
||||
// Check final status after following any redirects
|
||||
if (response.status >= 400) {
|
||||
// Build redirect info string if available
|
||||
const redirectInfo =
|
||||
response.redirects && response.redirects.length > 0
|
||||
? ` (redirected to: ${response.redirects.join(' -> ')})`
|
||||
: '';
|
||||
|
||||
handleFailedLink(href, response.status, 'regular', redirectInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Before all tests, initialize the report
|
||||
before(() => {
|
||||
cy.task('initializeBrokenLinksReport');
|
||||
});
|
||||
|
||||
subjects.forEach((subject) => {
|
||||
it(`contains valid internal links on ${subject}`, function () {
|
||||
cy.visit(`${subject}`);
|
||||
it(`${subject} has valid internal links`, function () {
|
||||
cy.visit(`${subject}`, { timeout: 20000 });
|
||||
|
||||
// Test internal links
|
||||
// 1. Timeout and fail the test if article is not found
|
||||
// 2. Check each link.
|
||||
// 3. If no links are found, continue without failing
|
||||
cy.get('article').then(($article) => {
|
||||
cy.get('article, .api-content').then(($article) => {
|
||||
// Find links without failing the test if none are found
|
||||
const $links = $article.find('a[href^="/"]');
|
||||
if ($links.length === 0) {
|
||||
cy.log('No internal links found on this page');
|
||||
return;
|
||||
}
|
||||
|
||||
// Now test each link
|
||||
cy.wrap($links).each(($a) => {
|
||||
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}`);
|
||||
|
||||
// Track missing anchors for summary
|
||||
const missingAnchors = [];
|
||||
// Define selectors for anchor links to ignore, such as behavior triggers
|
||||
const ignoreLinks = ['.tabs a[href^="#"]', '.code-tabs a[href^="#"]'];
|
||||
|
||||
// Process anchor links individually
|
||||
cy.get('article').then(($article) => {
|
||||
const $anchorLinks = $article.find('a[href^="#"]');
|
||||
const anchorSelector =
|
||||
'a[href^="#"]:not(' + ignoreLinks.join('):not(') + ')';
|
||||
|
||||
cy.get('article, .api-content').then(($article) => {
|
||||
const $anchorLinks = $article.find(anchorSelector);
|
||||
if ($anchorLinks.length === 0) {
|
||||
cy.log('No anchor links found on this page');
|
||||
return;
|
||||
}
|
||||
|
||||
cy.wrap($anchorLinks).each(($a) => {
|
||||
const href = $a.prop('href');
|
||||
if (href && href.length > 1) {
|
||||
// Skip empty anchors (#)
|
||||
// Get just the fragment part
|
||||
const url = new URL(href);
|
||||
const anchorId = url.hash.substring(1); // Remove the # character
|
||||
const href = $a.prop('href');
|
||||
const linkText = $a.text().trim();
|
||||
|
||||
if (!anchorId) {
|
||||
cy.log(`Skipping empty anchor in ${href}`);
|
||||
return;
|
||||
if (href && href.length > 1) {
|
||||
// Get just the fragment part
|
||||
const url = new URL(href);
|
||||
const anchorId = url.hash.substring(1); // Remove the # character
|
||||
|
||||
if (!anchorId) {
|
||||
cy.log(`Skipping empty anchor in ${href}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use DOM to check if the element exists
|
||||
cy.window().then((win) => {
|
||||
const element = win.document.getElementById(anchorId);
|
||||
if (!element) {
|
||||
cy.task('reportBrokenLink', {
|
||||
url: `#${anchorId}`,
|
||||
status: 404,
|
||||
type: 'anchor',
|
||||
linkText,
|
||||
page: subject,
|
||||
});
|
||||
cy.log(`⚠️ Missing anchor target: #${anchorId}`);
|
||||
}
|
||||
|
||||
// Use DOM to check if the element exists, but don't fail if missing
|
||||
cy.window().then((win) => {
|
||||
const element = win.document.getElementById(anchorId);
|
||||
if (element) {
|
||||
cy.log(`✅ Anchor target exists: #${anchorId}`);
|
||||
} else {
|
||||
// Just warn about the missing anchor
|
||||
cy.log(`⚠️ WARNING: Missing anchor target: #${anchorId}`);
|
||||
missingAnchors.push(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 () {
|
||||
cy.visit(`${subject}`);
|
||||
// Test external links
|
||||
// 1. Timeout and fail the test if article is not found
|
||||
// 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
|
||||
const $links = $article.find('a[href^="http"]');
|
||||
if ($links.length === 0) {
|
||||
cy.log('No external links found on this page');
|
||||
return;
|
||||
});
|
||||
}
|
||||
cy.wrap($links).each(($a) => {
|
||||
const href = $a.attr('href');
|
||||
testLink(href);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`${subject} has valid external links`, function () {
|
||||
cy.visit(`${subject}`);
|
||||
|
||||
// Test external links
|
||||
cy.get('article, .api-content').then(($article) => {
|
||||
// Find links without failing the test if none are found
|
||||
const $links = $article.find('a[href^="http"]');
|
||||
if ($links.length === 0) {
|
||||
cy.log('No external links found on this page');
|
||||
return;
|
||||
}
|
||||
|
||||
cy.debug(`Found ${$links.length} external links`);
|
||||
cy.wrap($links).each(($a) => {
|
||||
const href = $a.attr('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'
|
||||
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
|
||||
|
||||
import { execSync } from 'child_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
|
||||
const filePaths = process.argv.slice(2);
|
||||
const filePaths = process.argv.slice(2).filter((arg) => !arg.startsWith('--'));
|
||||
|
||||
// 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
|
||||
const contentFiles = filePaths.filter(file =>
|
||||
file.startsWith('content/') && (file.endsWith('.md') || file.endsWith('.html'))
|
||||
// Separate shared content files and regular content files
|
||||
const sharedContentFiles = filePaths.filter(
|
||||
(file) =>
|
||||
file.startsWith('content/shared/') &&
|
||||
(file.endsWith('.md') || file.endsWith('.html'))
|
||||
);
|
||||
|
||||
if (contentFiles.length === 0) {
|
||||
console.log('No content files to check.');
|
||||
const regularContentFiles = filePaths.filter(
|
||||
(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);
|
||||
}
|
||||
|
||||
// Map file paths to URL paths
|
||||
function mapFilePathToUrl(filePath) {
|
||||
// Remove content/ prefix
|
||||
// Map file paths to URL paths and source information
|
||||
function mapFilePathToUrlAndSource(filePath) {
|
||||
// Map to URL
|
||||
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;
|
||||
|
||||
// Extract source
|
||||
const source = extractSourceFromFile(filePath);
|
||||
|
||||
return { url, source };
|
||||
}
|
||||
|
||||
const urls = contentFiles.map(mapFilePathToUrl);
|
||||
const urlList = urls.join(',');
|
||||
const mappedFiles = pagesToTest.map(mapFilePathToUrlAndSource);
|
||||
|
||||
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:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
if (jsonMode) {
|
||||
console.log(JSON.stringify(mappedFiles, null, 2));
|
||||
} else {
|
||||
// Print URL and source info in a format that's easy to parse
|
||||
mappedFiles.forEach((item) => console.log(`${item.url}|${item.source}`));
|
||||
}
|
||||
|
|
|
@ -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 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[]} */
|
||||
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,
|
||||
|
||||
// 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
|
||||
baseName: pages
|
||||
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 := findRE "[^/]+.*?" .RelPermalink }}
|
||||
{{ $product := index $productPathData 0 }}
|
||||
|
@ -23,7 +13,7 @@
|
|||
{{ $products := .Site.Data.products }}
|
||||
{{ $influxdb_urls := .Site.Data.influxdb_urls }}
|
||||
<!-- Build main.js -->
|
||||
{{ with resources.Get "js/main.js" }}
|
||||
{{ with resources.Get "js/index.js" }}
|
||||
{{ $opts := dict
|
||||
"minify" hugo.IsProduction
|
||||
"sourceMap" (cond hugo.IsProduction "" "external")
|
||||
|
|
181
lefthook.yml
181
lefthook.yml
|
@ -1,135 +1,86 @@
|
|||
# Refer for explanation to following link:
|
||||
# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md
|
||||
#
|
||||
pre-push:
|
||||
commands:
|
||||
packages-audit:
|
||||
tags: frontend security
|
||||
run: yarn audit
|
||||
|
||||
pre-commit:
|
||||
parallel: true
|
||||
parallel: true
|
||||
commands:
|
||||
# Report linting warnings and errors, don't output files to stdout
|
||||
lint-markdown:
|
||||
tags: lint
|
||||
glob: "content/**/*.md"
|
||||
glob: 'content/*.md'
|
||||
run: |
|
||||
docker compose run --rm --name remark-lint remark-lint '{staged_files}'
|
||||
cloud-lint:
|
||||
tags: lint,v2
|
||||
glob: "content/influxdb/cloud/**/*.md"
|
||||
glob: 'content/influxdb/cloud/*.md'
|
||||
run: '.ci/vale/vale.sh
|
||||
--config=.vale.ini
|
||||
--minAlertLevel=error {staged_files}'
|
||||
cloud-dedicated-lint:
|
||||
tags: lint,v3
|
||||
glob: "content/influxdb/cloud-dedicated/**/*.md"
|
||||
glob: 'content/influxdb/cloud-dedicated/*.md'
|
||||
run: '.ci/vale/vale.sh
|
||||
--config=content/influxdb/cloud-dedicated/.vale.ini
|
||||
--minAlertLevel=error {staged_files}'
|
||||
cloud-serverless-lint:
|
||||
tags: lint,v3
|
||||
glob: "content/influxdb/cloud-serverless/**/*.md"
|
||||
glob: 'content/influxdb/cloud-serverless/*.md'
|
||||
run: '.ci/vale/vale.sh
|
||||
--config=content/influxdb/cloud-serverless/.vale.ini
|
||||
--minAlertLevel=error {staged_files}'
|
||||
clustered-lint:
|
||||
tags: lint,v3
|
||||
glob: "content/influxdb/clustered/**/*.md"
|
||||
glob: 'content/influxdb/clustered/*.md'
|
||||
run: '.ci/vale/vale.sh
|
||||
--config=content/influxdb/cloud-serverless/.vale.ini
|
||||
--minAlertLevel=error {staged_files}'
|
||||
telegraf-lint:
|
||||
tags: lint,clients
|
||||
glob: "content/telegraf/**/*.md"
|
||||
tags: lint,clients
|
||||
glob: 'content/telegraf/*.md'
|
||||
run: '.ci/vale/vale.sh
|
||||
--config=.vale.ini
|
||||
--minAlertLevel=error {staged_files}'
|
||||
v2-lint:
|
||||
tags: lint,v2
|
||||
glob: "content/influxdb/v2/**/*.md"
|
||||
glob: 'content/influxdb/v2/*.md'
|
||||
run: '.ci/vale/vale.sh
|
||||
--config=content/influxdb/v2/.vale.ini
|
||||
--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:
|
||||
tags: frontend,style
|
||||
glob: "*.{css,js,ts,jsx,tsx}"
|
||||
run: yarn prettier {staged_files}
|
||||
|
||||
build:
|
||||
tags: [frontend, style]
|
||||
glob: '*.{css,js,ts,jsx,tsx}'
|
||||
run: |
|
||||
yarn prettier --write --loglevel silent "{staged_files}" > /dev/null 2>&1 ||
|
||||
{ echo "⚠️ Prettier found formatting issues. Automatic formatting applied."
|
||||
git add {staged_files}
|
||||
}
|
||||
pre-push:
|
||||
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:
|
||||
priority: 1
|
||||
tags: test
|
||||
|
@ -137,6 +88,48 @@ build:
|
|||
--filter label=tag=influxdata-docs
|
||||
--filter status=exited | xargs docker rm)
|
||||
|| true'
|
||||
rebuild-test-images:
|
||||
build-pytest-image:
|
||||
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}'
|
49
package.json
49
package.json
|
@ -24,6 +24,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.4",
|
||||
"gray-matter": "^4.0.3",
|
||||
"jquery": "^3.7.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
@ -33,16 +34,48 @@
|
|||
"vanillajs-datepicker": "^1.3.4"
|
||||
},
|
||||
"scripts": {
|
||||
"e2e:chrome": "npx cypress run --browser chrome",
|
||||
"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",
|
||||
"build:pytest:image":"docker build -t influxdata/docs-pytest:latest -f Dockerfile.pytest .",
|
||||
"lint": "LEFTHOOK_EXCLUDE=test lefthook run pre-commit && lefthook run pre-push",
|
||||
"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": {
|
||||
"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"
|
||||
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:
|
||||
version "2.0.1"
|
||||
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"
|
||||
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:
|
||||
version "1.6.0"
|
||||
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"
|
||||
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:
|
||||
version "3.0.2"
|
||||
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"
|
||||
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:
|
||||
version "0.5.2"
|
||||
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:
|
||||
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:
|
||||
version "2.1.1"
|
||||
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"
|
||||
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:
|
||||
version "4.1.0"
|
||||
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"
|
||||
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:
|
||||
version "1.8.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.6"
|
||||
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"
|
||||
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:
|
||||
version "15.4.11"
|
||||
resolved "https://registry.yarnpkg.com/sql-formatter/-/sql-formatter-15.4.11.tgz#10a8205aa82d60507811360d4735e81d4a21c137"
|
||||
|
@ -3814,6 +3874,11 @@ strip-ansi@^7.0.1:
|
|||
dependencies:
|
||||
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:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5"
|
||||
|
|
Loading…
Reference in New Issue