Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
28d83ef
first pass for vault exclusions
williamdalessandro Sep 17, 2025
bf00207
added in functionality to have the exclusion work and play nice with …
williamdalessandro Sep 17, 2025
8029005
adding in manual test case to see if vault exclusion is actually working
williamdalessandro Sep 17, 2025
f826f97
added test case to terraform exclusion
williamdalessandro Sep 17, 2025
7a5924d
added in test suite for vault exclusions- currently failing
williamdalessandro Sep 17, 2025
ca1e6f4
Merge branch 'main' into adding-vault-content-inclusion-exclusions
williamdalessandro Sep 17, 2025
bd5f9b6
fixing tests, 2 more left
williamdalessandro Sep 18, 2025
1770d45
final test fixes, removing some comments
williamdalessandro Sep 18, 2025
60818e9
Merge branch 'main' into adding-vault-content-inclusion-exclusions
williamdalessandro Sep 18, 2025
e174739
adding in new compiled binaries since these are changes in the script…
williamdalessandro Sep 18, 2025
71ef912
updating the single file transform to include vault exclusions
williamdalessandro Sep 18, 2025
c55f4c4
Merge branch 'main' into adding-vault-content-inclusion-exclusions
williamdalessandro Sep 18, 2025
51b0a7d
slight comment change
williamdalessandro Sep 18, 2025
f321df5
adding latest binaries
williamdalessandro Sep 18, 2025
a0008ca
adding shared mjs file
williamdalessandro Sep 19, 2025
550b5c4
added revelant comment to shared.mjs, renamed VLT to vault
williamdalessandro Sep 19, 2025
76eb25a
forgot to add copyright header
williamdalessandro Sep 19, 2025
d2611de
Move config to productConfig
RubenSandwich Sep 22, 2025
8516798
Refactor version test code
RubenSandwich Sep 22, 2025
e115082
it's time to walk away
RubenSandwich Sep 22, 2025
04434cb
small changes
williamdalessandro Sep 23, 2025
80e1740
adding reducing code duplication and added readme explainer
williamdalessandro Sep 23, 2025
c3c0efa
adding in integration with existing code
williamdalessandro Sep 23, 2025
1d636f6
current binaries for build to reflect latest changes
williamdalessandro Sep 23, 2025
7e23f2d
initial changes for unified approach
williamdalessandro Sep 25, 2025
8371a05
massive structural change, turned shared functionality into shared ut…
williamdalessandro Sep 25, 2025
c5c2bb4
adding in individual file transform support
williamdalessandro Sep 25, 2025
8a563bb
merging changes back to primary branch
williamdalessandro Sep 25, 2025
c4692d1
staying consistent until the order should be properly changed
williamdalessandro Sep 25, 2025
4746560
removing older approach
williamdalessandro Sep 25, 2025
09fe52f
adding simple change to content exclusion
williamdalessandro Oct 2, 2025
a481a98
adding in change to do partials first and skip global partials
williamdalessandro Oct 2, 2025
2bd33e6
adding in two extra tests for exclude content to try to catch parsing…
williamdalessandro Oct 2, 2025
33634e8
adding in entire testing suite for mdx transforms to test against thi…
williamdalessandro Oct 2, 2025
3e92a7f
fixing bug caught by tests during global partials- certain partials w…
williamdalessandro Oct 2, 2025
d5c21df
update readme
williamdalessandro Oct 2, 2025
d32ac47
adding in global partial support for single file transform
williamdalessandro Oct 2, 2025
85a51fa
change formatting in mdx transform test, added in more test for edge …
williamdalessandro Oct 2, 2025
89f0d34
adding in new previously failing test cases
williamdalessandro Oct 6, 2025
af5e34f
edge case additions to AST traversal, small formatting changes
williamdalessandro Oct 6, 2025
96188c4
updated readme
williamdalessandro Oct 6, 2025
9b3a74a
removing antiquated approach
williamdalessandro Oct 6, 2025
747ece3
commenting out broken tests for now
williamdalessandro Oct 7, 2025
8f1f278
adding in failing tests, moved test 3 to correct spot
williamdalessandro Oct 7, 2025
d4adece
adding in debug logs, moved out logic for determining whether node is…
williamdalessandro Oct 7, 2025
4ec404c
update readme
williamdalessandro Oct 7, 2025
3f8ea8f
found uncaught reference that is now exposed due to partials/content …
williamdalessandro Oct 7, 2025
d064365
updating prebuild binaries
williamdalessandro Oct 7, 2025
b930892
avoiding conflict
williamdalessandro Oct 8, 2025
855e806
adding binaries from main
williamdalessandro Oct 8, 2025
b9d1b9b
Merge branch 'main' into adding-vault-content-inclusion-exclusions
williamdalessandro Oct 8, 2025
9e341e3
updating binaries
williamdalessandro Oct 8, 2025
4f70c87
removing testing artifacts
williamdalessandro Oct 9, 2025
4d7ce31
comments and style changes
williamdalessandro Oct 9, 2025
6819c64
adding in tests to use unmocked product config
williamdalessandro Oct 9, 2025
37f9d37
update readme
williamdalessandro Oct 9, 2025
535e53b
Remove .gitignore changes from PR
williamdalessandro Oct 9, 2025
bdac35d
Merge branch 'main' into adding-vault-content-inclusion-exclusions
williamdalessandro Oct 9, 2025
35dfbb8
update prebuild binaries off latest main
williamdalessandro Oct 9, 2025
925f357
update readme
williamdalessandro Oct 9, 2025
7e543f0
Merge branch 'main' into adding-vault-content-inclusion-exclusions
williamdalessandro Oct 10, 2025
8556ae9
update prebuild binaries
williamdalessandro Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
page_title: Test Vault Content Exclusion
---

# Test Vault Content Exclusion

This content should always appear.

<!-- BEGIN: Vault:>=v1.21.x -->
This content should be REMOVED because current version (1.20.x) is less than 1.21.x
<!-- END: Vault:>=v1.21.x -->

<!-- BEGIN: Vault:<=v1.21.x -->
This content should STAY because current version (1.20.x) is less than or equal to 1.21.x
<!-- END: Vault:<=v1.21.x -->

<!-- BEGIN: Vault:=v1.20.x -->
This content should STAY because current version equals 1.20.x
<!-- END: Vault:=v1.20.x -->

<!-- BEGIN: Vault:<v1.19.x -->
This content should be REMOVED because current version (1.20.x) is not less than 1.19.x
<!-- END: Vault:<v1.19.x -->

Final content that should always appear.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
loadRedirects,
} from './rewrite-internal-redirects/rewrite-internal-redirects.mjs'
import { transformExcludeTerraformContent } from './exclude-terraform-content/index.mjs'
import { transformExcludeVaultContent } from './exclude-vault-content/index.mjs'

const CWD = process.cwd()
const VERSION_METADATA_FILE = path.join(CWD, 'app/api/versionMetadata.json')
Expand Down Expand Up @@ -89,6 +90,7 @@ export async function applyFileMdxTransforms(entry, versionMetadata = {}) {
const remarkResults = await remark()
.use(remarkMdx)
.use(transformExcludeTerraformContent, { filePath })
.use(transformExcludeVaultContent, { filePath, version })
.use(remarkIncludePartialsPlugin, { partialsDir, filePath })
.use(paragraphCustomAlertsPlugin)
.use(rewriteInternalRedirectsPlugin, {
Expand Down
2 changes: 2 additions & 0 deletions scripts/prebuild/mdx-transforms/build-mdx-transforms.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
loadRedirects,
} from './rewrite-internal-redirects/rewrite-internal-redirects.mjs'
import { transformExcludeTerraformContent } from './exclude-terraform-content/index.mjs'
import { transformExcludeVaultContent } from './exclude-vault-content/index.mjs'

import { PRODUCT_CONFIG } from '#productConfig.mjs'

Expand Down Expand Up @@ -135,6 +136,7 @@ async function applyMdxTransforms(entry, versionMetadata = {}) {
const remarkResults = await remark()
.use(remarkMdx)
.use(transformExcludeTerraformContent, { filePath })
.use(transformExcludeVaultContent, { filePath, version })
.use(remarkIncludePartialsPlugin, { partialsDir, filePath })
.use(paragraphCustomAlertsPlugin)
.use(rewriteInternalRedirectsPlugin, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import visit from 'unist-util-visit'
import { DIRECTIVE_PRODUCTS } from '../shared.mjs'

// this is a courtesy wrapper to prepend error messages
class ExcludeTerraformContentError extends Error {
Expand All @@ -21,6 +22,7 @@ export const BEGIN_RE = /^(\s+)?<!--\s+BEGIN:\s+(?<block>.*?)\s+-->(\s+)?$/
export const END_RE = /^(\s+)?<!--\s+END:\s+(?<block>.*?)\s+-->(\s+)?$/
export const DIRECTIVE_RE = /(?<exclusion>TFC|TFEnterprise):only/i

// Adding the directive products parameter to allow for extensibility in tests
export function transformExcludeTerraformContent({ filePath }) {
return function transformer(tree) {
// accumulate the content exclusion blocks
Expand Down Expand Up @@ -125,6 +127,12 @@ export function transformExcludeTerraformContent({ filePath }) {

// TODO: line start and end do not take into account front matter, as it is just tree parsing and technically front matter is not part of the MDX tree
if (!directive) {
// Check if this is a product we should handle
const productMatch = flag.match(/^(\w+):/)

if (productMatch && DIRECTIVE_PRODUCTS.includes(productMatch[1])) {
return // Skip this directive - it's for another product
}
throw new ExcludeTerraformContentError(
`Directive block ${block} could not be parsed between lines ${start} and ${end}`,
tree,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import { transformExcludeTerraformContent } from './index.mjs'
import remark from 'remark'
import remarkMdx from 'remark-mdx'

// Mock for testing custom directive products
vi.mock('../build-mdx-transforms.mjs', async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
DIRECTIVE_PRODUCTS: ['Vault', 'TFC', 'TFEnterprise'], // Default for most tests
}
})

const runTransform = async (markdown, filePath) => {
const processor = await remark()
.use(remarkMdx)
Expand Down Expand Up @@ -222,4 +231,39 @@ This content should stay.
const result = await runTransform(markdown, filePath)
expect(result.trim()).toBe(expected.trim())
})

it('should throw an error for directives with products not in directiveProducts array', async () => {
// Override DIRECTIVE_PRODUCTS for this test only
vi.doMock('../build-mdx-transforms.mjs', async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
DIRECTIVE_PRODUCTS: ['TFC', 'TFEnterprise'], // CONSUL not included
}
})

// Need to re-import the transform after mocking
const { transformExcludeTerraformContent: mockTransform } = await import(
'./index.mjs'
)

const markdown = `
<!-- BEGIN: CONSUL:only -->
This should cause an error - CONSUL not in directiveProducts.
<!-- END: CONSUL:only -->
`
const customRunTransform = async (markdown, filePath) => {
const processor = await remark()
.use(remarkMdx)
.use(mockTransform, { filePath })
.process(markdown)
return processor.contents
}

await expect(async () => {
return await customRunTransform(markdown, ptfeFilePath)
}).rejects.toThrow(
'Directive block CONSUL:only could not be parsed between lines 2 and 4',
)
})
})
242 changes: 242 additions & 0 deletions scripts/prebuild/mdx-transforms/exclude-vault-content/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import visit from 'unist-util-visit'
import { SemVer, gt, gte, lt, lte, eq } from 'semver'
import { DIRECTIVE_PRODUCTS } from '../shared.mjs'

// this is a courtesy wrapper to prepend error messages
class ExcludeVaultContentError extends Error {
constructor(message, markdownSource) {
super(
`[strip-vault-content] ${message}` +
`\n- ${markdownSource}` +
`\n- ${markdownSource}`,
)
this.name = 'ExcludeVaultContentError'
}
}

export const BEGIN_RE = /^(\s+)?<!--\s+BEGIN:\s+(?<block>.*?)\s+-->(\s+)?$/
export const END_RE = /^(\s+)?<!--\s+END:\s+(?<block>.*?)\s+-->(\s+)?$/
export const DIRECTIVE_RE =
/^(?<product>Vault):(?<comparator><=|>=|<|>|=)v(?<version>(\d+)\.(\d+)\.x)$/i

// Adding the directive products parameter to allow for extensibility in tests
export function transformExcludeVaultContent({ filePath, version }) {
return function transformer(tree) {
// accumulate the content exclusion blocks
/** @type ({ start: number; block: string; end: number })[] */
const matches = []
let matching = false
let block = ''

visit(tree, (node) => {
const nodeValue = node.value
const nodeIndex = node.position?.end?.line

if (!nodeValue || !nodeIndex || !filePath.includes('vault')) {
return
}

if (!matching) {
// Wait for a BEGIN block to be matched

// throw if an END block is matched first
const endMatch = nodeValue.match(END_RE)
if (endMatch) {
throw new ExcludeVaultContentError(
`Unexpected END block: line ${nodeIndex}`,
tree,
)
}

const beginMatch = nodeValue.match(BEGIN_RE)

if (beginMatch) {
matching = true

if (!beginMatch.groups?.block) {
throw new ExcludeVaultContentError(
'No block could be parsed from BEGIN comment',
tree,
)
}

block = beginMatch.groups.block

matches.push({
start: nodeIndex,
block: beginMatch.groups.block,
end: -1,
})
}
} else {
// If we are actively matching within a block, monitor for the end

// throw if a BEGIN block is matched again
const beginMatch = nodeValue.match(BEGIN_RE)
if (beginMatch) {
throw new ExcludeVaultContentError(
`Unexpected BEGIN block: line ${nodeIndex}`,
tree,
)
}

const endMatch = nodeValue.match(END_RE)
if (endMatch) {
const latestMatch = matches[matches.length - 1]

if (!endMatch.groups?.block) {
throw new ExcludeVaultContentError(
'No block could be parsed from END comment',
tree,
)
}

// If we reach and end with an un-matching block name, throw an error
if (endMatch.groups.block !== block) {
const errMsg =
`Mismatched block names: Block opens with "${block}", and closes with "${endMatch.groups.block}".` +
`\n` +
`Please make sure opening and closing block names are matching. Blocks cannot be nested.` +
`\n` +
`- Open: ${latestMatch.start}: ${block}` +
`\n` +
`- Close: ${nodeIndex}: ${endMatch.groups.block}` +
`\n`
console.error(errMsg)
throw new ExcludeVaultContentError('Mismatched block names', tree)
}

// Push the ending index of the block into the match result and set matching to false
latestMatch.end = nodeIndex
block = ''
matching = false
}
}
})

// iterate through the list of matches backwards to remove lines
matches.reverse().forEach(({ start, end, block }) => {
const [flag] = block.split(/\s+/)
const directive = flag.match(DIRECTIVE_RE)

if (!directive?.groups) {
// Check if this is a product we should handle
const productMatch = flag.match(/^(\w+):/)

// If the product matches the current one we care about 'Vault' then
// continue with further checks on the version and comparator
if (productMatch && productMatch[1] === 'Vault') {
// This is our product, but directive didn't match - check if it's a version format issue
const versionFormatCheck = flag.match(/^(\w+):(<=|>=|<|>|=)v(.+)$/)

if (versionFormatCheck) {
const [, , , versionPart] = versionFormatCheck
// Check if version format is invalid (not X.Y.x pattern)
if (!versionPart.match(/^\d+\.\d+\.x$/)) {
throw new ExcludeVaultContentError(
`Invalid version format in directive: ${flag}. Expected format: vX.Y.x`,
tree,
)
}
}
// If we get here, it's some other directive format error
throw new ExcludeVaultContentError(
`Invalid directive format: ${flag}`,
tree,
)
}

// otherwise if it is in the directive products list, skip it
else if (productMatch && DIRECTIVE_PRODUCTS.includes(productMatch[1])) {
return // Skip this directive - it's for another product
}

// else if is not in the product list, throw this error
throw new ExcludeVaultContentError(
`Directive block ${block} could not be parsed between lines ${start} and ${end}`,
tree,
)
}

// This is the version that is parsed from reading the filename
const currentVersion = version || ''

// This is the directive that is parsed from the exclusion block
const { comparator, version: directiveVersion } = directive.groups

try {
const versionSemVer = getTfeSemver(currentVersion)
const directiveSemVer = getTfeSemver(directiveVersion)
const compare = getComparisonFn(comparator, tree)

const shouldKeepContent = compare(versionSemVer, directiveSemVer)

// If the version comparison fails, remove the content
if (!shouldKeepContent) {
function removeNodesInRange(nodes) {
for (let i = nodes.length - 1; i >= 0; i--) {
const node = nodes[i]
if (
node.position &&
node.position.start.line >= start &&
node.position.end.line <= end
) {
nodes.splice(i, 1)
} else if (node.children && Array.isArray(node.children)) {
removeNodesInRange(node.children, node)
}
}
}
removeNodesInRange(tree.children, tree)
}
} catch (error) {
throw new ExcludeVaultContentError(
`Version comparison failed: ${error.message}`,
tree,
)
}
})
return tree
}
}

const getTfeSemver = (version) => {
// Handle version strings like "1.20.x" by converting to "1.20.0"
const normalized = version.replace(/\.x$/, '.0')
return new SemVer(normalized)
}

const getComparisonFn = (operator, document) => {
switch (operator) {
case '<=':
return (a, b) => {
return lte(a, b)
}
case '>=':
return (a, b) => {
return gte(a, b)
}
case '<':
return (a, b) => {
return lt(a, b)
}
case '>':
return (a, b) => {
return gt(a, b)
}
case '=':
return (a, b) => {
return eq(a, b)
}
default:
throw new ExcludeVaultContentError(
'Invalid comparator: ' + operator,
document,
)
}
}
Loading
Loading