diff --git a/.changeset/fancy-keys-hug.md b/.changeset/fancy-keys-hug.md new file mode 100644 index 0000000000..59c216edc0 --- /dev/null +++ b/.changeset/fancy-keys-hug.md @@ -0,0 +1,5 @@ +--- +"@patternfly/pfe-tools": minor +--- + +Accept `tagPrefix` as an array of strings, supporting repos with multiple element prefixes diff --git a/.pfe.config.json b/.pfe.config.json index 02fa929c23..396a061159 100644 --- a/.pfe.config.json +++ b/.pfe.config.json @@ -1,4 +1,4 @@ { "renderTitleInOverview": false, - "tagPrefix": "pf-v5" + "tagPrefix": ["pf-v5", "pf-v6"] } diff --git a/tools/pfe-tools/11ty/DocsPage.ts b/tools/pfe-tools/11ty/DocsPage.ts index 108687687b..10a0e8ae97 100644 --- a/tools/pfe-tools/11ty/DocsPage.ts +++ b/tools/pfe-tools/11ty/DocsPage.ts @@ -1,5 +1,5 @@ import type { PfeConfig } from '../config.js'; -import { getPfeConfig } from '../config.js'; +import { getPfeConfig, matchPrefix } from '../config.js'; import { Manifest } from '../custom-elements-manifest/lib/Manifest.js'; import slugify from 'slugify'; @@ -26,7 +26,7 @@ export class DocsPage { this.docsTemplatePath = options?.docsTemplatePath; this.summary = this.manifest.getSummary(this.tagName); this.description = this.manifest.getDescription(this.tagName); - const prefix = `${config.tagPrefix.replace(/-$/, '')}-`; + const prefix = matchPrefix(this.tagName, config); const aliased = config.aliases[this.tagName] ?? this.tagName.replace(prefix, ''); this.title = options?.title ?? Manifest.prettyTag(this.tagName, config.aliases, prefix); this.slug = slugify(aliased, { strict: true, lower: true }); diff --git a/tools/pfe-tools/11ty/plugins/types.ts b/tools/pfe-tools/11ty/plugins/types.ts index f8ac6ef0f1..4eb3786ef2 100644 --- a/tools/pfe-tools/11ty/plugins/types.ts +++ b/tools/pfe-tools/11ty/plugins/types.ts @@ -1,17 +1,7 @@ import type { PfeConfig } from '@patternfly/pfe-tools/config'; -import type { Manifest } from '@patternfly/pfe-tools/custom-elements-manifest/lib/Manifest.js'; -export interface DemoRecord { - title: string; - tagName: string; - tagPrefix: string; - primaryElementName: string; - manifest: Manifest; - slug: string; - filePath: string; - permalink: string; - url: string; -} +export type { DemoRecord } from '@patternfly/pfe-tools/custom-elements-manifest/lib/Manifest.js'; +import type { DemoRecord } from '@patternfly/pfe-tools/custom-elements-manifest/lib/Manifest.js'; export interface PluginOptions extends PfeConfig { /** list of extra demo records not included in the custom-elements-manifest. Default [] */ diff --git a/tools/pfe-tools/config.spec.ts b/tools/pfe-tools/config.spec.ts new file mode 100644 index 0000000000..f72350a5cf --- /dev/null +++ b/tools/pfe-tools/config.spec.ts @@ -0,0 +1,75 @@ +import { describe, it } from 'node:test'; +import { strict as assert } from 'node:assert'; + +import { getPrefixes, matchPrefix, deslugify } from './config.js'; + +describe('getPrefixes', function() { + it('should return array from single string', function() { + assert.deepStrictEqual(getPrefixes({ tagPrefix: 'pf' }), ['pf']); + }); + + it('should return array as-is', function() { + assert.deepStrictEqual(getPrefixes({ tagPrefix: ['pf-v5', 'pf-v6'] }), ['pf-v5', 'pf-v6']); + }); + + it('should filter empty strings', function() { + assert.deepStrictEqual(getPrefixes({ tagPrefix: ['pf-v5', '', 'pf-v6'] }), ['pf-v5', 'pf-v6']); + }); + + it('should throw when tagPrefix is undefined', function() { + assert.throws(() => getPrefixes({ tagPrefix: undefined }), { + message: 'tagPrefix must contain at least one non-empty prefix', + }); + }); + + it('should throw when tagPrefix is empty array', function() { + assert.throws(() => getPrefixes({ tagPrefix: [] }), { + message: 'tagPrefix must contain at least one non-empty prefix', + }); + }); + + it('should throw when all entries are empty strings', function() { + assert.throws(() => getPrefixes({ tagPrefix: ['', ''] }), { + message: 'tagPrefix must contain at least one non-empty prefix', + }); + }); +}); + +describe('matchPrefix', function() { + it('should match pf-v5 prefix', function() { + assert.strictEqual(matchPrefix('pf-v5-button', { tagPrefix: ['pf-v5', 'pf-v6'] }), 'pf-v5-'); + }); + + it('should match pf-v6 prefix', function() { + assert.strictEqual(matchPrefix('pf-v6-card', { tagPrefix: ['pf-v5', 'pf-v6'] }), 'pf-v6-'); + }); + + it('should fall back to first prefix when no match', function() { + assert.strictEqual(matchPrefix('custom-element', { tagPrefix: ['pf-v5', 'pf-v6'] }), 'pf-v5-'); + }); + + it('should work with single string prefix', function() { + assert.strictEqual(matchPrefix('pf-button', { tagPrefix: 'pf' }), 'pf-'); + }); + + it('should strip trailing dash from config before adding canonical dash', function() { + assert.strictEqual(matchPrefix('pf-v5-button', { tagPrefix: 'pf-v5-' }), 'pf-v5-'); + }); +}); + +describe('deslugify', function() { + it('should return prefixed slug when slug already has a configured prefix', function() { + const result = deslugify('pf-v5-button'); + assert.strictEqual(result, 'pf-v5-button'); + }); + + it('should return prefixed slug for pf-v6 prefix', function() { + const result = deslugify('pf-v6-card'); + assert.strictEqual(result, 'pf-v6-card'); + }); + + it('should prepend first prefix when slug has no prefix', function() { + const result = deslugify('button'); + assert.strictEqual(result, 'pf-v5-button'); + }); +}); diff --git a/tools/pfe-tools/config.ts b/tools/pfe-tools/config.ts index 11921bfdff..e63be69a01 100644 --- a/tools/pfe-tools/config.ts +++ b/tools/pfe-tools/config.ts @@ -30,8 +30,8 @@ export interface PfeConfig { sourceControlURLPrefix?: string ; /** absolute URL prefix for demos, with trailing slash. Default 'https://patternflyelements.org/' */ demoURLPrefix?: string ; - /** custom elements namespace. Default 'pf' */ - tagPrefix?: string; + /** custom elements namespace. Default 'pf'. Accepts an array for repos with multiple prefixes. */ + tagPrefix?: string | string[]; /** Dev Server site options */ site?: SiteOptions; } @@ -78,6 +78,27 @@ export function getPfeConfig(rootDir: string = process.cwd()): Required): string[] { + const prefixes = [config.tagPrefix].flat().filter((p): p is string => !!p); + if (!prefixes.length) { + throw new Error('tagPrefix must contain at least one non-empty prefix'); + } + return prefixes; +} + +/** + * Returns the prefix that matches the given tag name (with trailing dash), + * or falls back to the first configured prefix. + */ +export function matchPrefix(tagName: string, config: Pick): string { + const prefixes = getPrefixes(config).map(p => `${p.replace(/-$/, '')}-`); + return prefixes.find(p => tagName.startsWith(p)) ?? prefixes[0]; +} + const slugsConfigMap = new Map }>(); const reverseSlugifyObject = ([k, v]: [string, string]): [string, string] => [slugify(v, { lower: true }), k]; @@ -102,6 +123,8 @@ export function deslugify( rootDir: string = process.cwd(), ): string { const { slugs, config } = getSlugsMap(rootDir); - const prefixedSlug = (slug.startsWith(`${config.tagPrefix}-`)) ? slug : `${config.tagPrefix}-${slug}`; + const prefixes = getPrefixes(config); + const hasPrefix = prefixes.some(p => slug.startsWith(`${p}-`)); + const prefixedSlug = hasPrefix ? slug : `${prefixes[0]}-${slug}`; return slugs.get(slug) ?? prefixedSlug; } diff --git a/tools/pfe-tools/custom-elements-manifest/lib/Manifest.ts b/tools/pfe-tools/custom-elements-manifest/lib/Manifest.ts index 032e3ccfeb..79ff7d921e 100644 --- a/tools/pfe-tools/custom-elements-manifest/lib/Manifest.ts +++ b/tools/pfe-tools/custom-elements-manifest/lib/Manifest.ts @@ -21,7 +21,7 @@ import { readFileSync } from 'node:fs'; import { getAllPackages } from './get-all-packages.js'; import slugify from 'slugify'; -import { deslugify } from '@patternfly/pfe-tools/config.js'; +import { deslugify, matchPrefix } from '@patternfly/pfe-tools/config.js'; type PredicateFn = (x: unknown) => boolean; @@ -324,7 +324,7 @@ export class Manifest { const [last = ''] = filePath.split(path.sep).reverse(); const filename = last.replace('.html', ''); const isMainElementDemo = filename === 'index'; - const prefix = `${options.tagPrefix.replace(/-$/, '')}-`; + const prefix = matchPrefix(tagName, options); const title = isMainElementDemo ? prettyTag(tagName, options.aliases, prefix) : last .replace(/(?:^|[-/\s])\w/g, x => x.toUpperCase()) diff --git a/tools/pfe-tools/test/config.ts b/tools/pfe-tools/test/config.ts index cfce355d59..b9b66f1ecf 100644 --- a/tools/pfe-tools/test/config.ts +++ b/tools/pfe-tools/test/config.ts @@ -8,7 +8,7 @@ import { junitReporter } from '@web/test-runner-junit-reporter'; import { a11ySnapshotPlugin } from '@web/test-runner-commands/plugins'; import { pfeDevServerConfig, type PfeDevServerConfigOptions } from '../dev-server/config.js'; -import { getPfeConfig } from '../config.js'; +import { getPfeConfig, getPrefixes } from '../config.js'; export interface PfeTestRunnerConfigOptions extends PfeDevServerConfigOptions { files?: string[]; @@ -50,7 +50,9 @@ const exists = async (path: string | URL) => { export function pfeTestRunnerConfig(opts: PfeTestRunnerConfigOptions): TestRunnerConfig { const { open, ...devServerConfig } = pfeDevServerConfig({ ...opts, loadDemo: false }); - const { elementsDir, tagPrefix } = getPfeConfig(); + const config = getPfeConfig(); + const { elementsDir } = config; + const tagPrefixes = getPrefixes(config); const configuredReporter = opts.reporter ?? 'default'; @@ -121,7 +123,7 @@ export function pfeTestRunnerConfig(opts: PfeTestRunnerConfigOptions): TestRunne */ async function(ctx, next) { if (ctx.path.endsWith('.js') - && ctx.path.startsWith(`/${elementsDir}/${tagPrefix}-`) + && tagPrefixes.some(p => ctx.path.startsWith(`/${elementsDir}/${p}-`)) && await exists(`./${ctx.path}`.replace('.js', '.ts').replace('//', '/'))) { ctx.redirect(ctx.path.replace('.js', '.ts')); } else { diff --git a/web-test-runner.config.js b/web-test-runner.config.js index 57e1cd9cea..a6445088f8 100644 --- a/web-test-runner.config.js +++ b/web-test-runner.config.js @@ -5,7 +5,7 @@ import { getPatternflyIconNodemodulesImports } from '@patternfly/pfe-tools/dev-s export default pfeTestRunnerConfig({ // workaround for https://github.com/evanw/esbuild/issues/3019 tsconfig: 'tsconfig.esbuild.json', - files: ['!tools/create-element/templates/**/*'], + files: ['!tools/create-element/templates/**/*', '!tools/pfe-tools/*.spec.ts'], reporter: process.env.CI ? 'summary' : 'default', importMapOptions: { providers: {