diff --git a/oxfmt.config.ts b/oxfmt.config.ts index 1d9fe25f..91243c55 100644 --- a/oxfmt.config.ts +++ b/oxfmt.config.ts @@ -1,3 +1,7 @@ +// wbfy:start oxfmt-base import config from '@willbooster/oxfmt-config'; +// wbfy:end oxfmt-base +// wbfy:start oxfmt-export export default config; +// wbfy:end oxfmt-export diff --git a/packages/shared-lib-blitz-next/oxfmt.config.ts b/packages/shared-lib-blitz-next/oxfmt.config.ts index 1d9fe25f..91243c55 100644 --- a/packages/shared-lib-blitz-next/oxfmt.config.ts +++ b/packages/shared-lib-blitz-next/oxfmt.config.ts @@ -1,3 +1,7 @@ +// wbfy:start oxfmt-base import config from '@willbooster/oxfmt-config'; +// wbfy:end oxfmt-base +// wbfy:start oxfmt-export export default config; +// wbfy:end oxfmt-export diff --git a/packages/shared-lib-next/oxfmt.config.ts b/packages/shared-lib-next/oxfmt.config.ts index 1d9fe25f..91243c55 100644 --- a/packages/shared-lib-next/oxfmt.config.ts +++ b/packages/shared-lib-next/oxfmt.config.ts @@ -1,3 +1,7 @@ +// wbfy:start oxfmt-base import config from '@willbooster/oxfmt-config'; +// wbfy:end oxfmt-base +// wbfy:start oxfmt-export export default config; +// wbfy:end oxfmt-export diff --git a/packages/shared-lib-node/oxfmt.config.ts b/packages/shared-lib-node/oxfmt.config.ts index 1d9fe25f..91243c55 100644 --- a/packages/shared-lib-node/oxfmt.config.ts +++ b/packages/shared-lib-node/oxfmt.config.ts @@ -1,3 +1,7 @@ +// wbfy:start oxfmt-base import config from '@willbooster/oxfmt-config'; +// wbfy:end oxfmt-base +// wbfy:start oxfmt-export export default config; +// wbfy:end oxfmt-export diff --git a/packages/shared-lib-react/oxfmt.config.ts b/packages/shared-lib-react/oxfmt.config.ts index 1d9fe25f..91243c55 100644 --- a/packages/shared-lib-react/oxfmt.config.ts +++ b/packages/shared-lib-react/oxfmt.config.ts @@ -1,3 +1,7 @@ +// wbfy:start oxfmt-base import config from '@willbooster/oxfmt-config'; +// wbfy:end oxfmt-base +// wbfy:start oxfmt-export export default config; +// wbfy:end oxfmt-export diff --git a/packages/shared-lib/oxfmt.config.ts b/packages/shared-lib/oxfmt.config.ts index 1d9fe25f..91243c55 100644 --- a/packages/shared-lib/oxfmt.config.ts +++ b/packages/shared-lib/oxfmt.config.ts @@ -1,3 +1,7 @@ +// wbfy:start oxfmt-base import config from '@willbooster/oxfmt-config'; +// wbfy:end oxfmt-base +// wbfy:start oxfmt-export export default config; +// wbfy:end oxfmt-export diff --git a/packages/wb/oxfmt.config.ts b/packages/wb/oxfmt.config.ts index 1d9fe25f..91243c55 100644 --- a/packages/wb/oxfmt.config.ts +++ b/packages/wb/oxfmt.config.ts @@ -1,3 +1,7 @@ +// wbfy:start oxfmt-base import config from '@willbooster/oxfmt-config'; +// wbfy:end oxfmt-base +// wbfy:start oxfmt-export export default config; +// wbfy:end oxfmt-export diff --git a/packages/wbfy/oxfmt.config.ts b/packages/wbfy/oxfmt.config.ts index 1d9fe25f..91243c55 100644 --- a/packages/wbfy/oxfmt.config.ts +++ b/packages/wbfy/oxfmt.config.ts @@ -1,3 +1,7 @@ +// wbfy:start oxfmt-base import config from '@willbooster/oxfmt-config'; +// wbfy:end oxfmt-base +// wbfy:start oxfmt-export export default config; +// wbfy:end oxfmt-export diff --git a/packages/wbfy/src/generators/configContent.ts b/packages/wbfy/src/generators/configContent.ts new file mode 100644 index 00000000..569425c3 --- /dev/null +++ b/packages/wbfy/src/generators/configContent.ts @@ -0,0 +1,3 @@ +export function normalizeConfigContent(content: string | undefined): string | undefined { + return content?.trim(); +} diff --git a/packages/wbfy/src/generators/managedConfigBlock.ts b/packages/wbfy/src/generators/managedConfigBlock.ts new file mode 100644 index 00000000..77524d4a --- /dev/null +++ b/packages/wbfy/src/generators/managedConfigBlock.ts @@ -0,0 +1,89 @@ +export type ConfigBlockName = 'base' | 'export'; + +interface ManagedConfigBlocksOptions { + blockNames: readonly ConfigBlockName[]; + markerPrefix: string; + toolName: string; +} + +interface GetConfigContentOptions { + desiredContent: string; + existingContent: string | undefined; + filePath: string; +} + +export class ManagedConfigBlocks { + private readonly blockNames: readonly ConfigBlockName[]; + + private readonly markerPrefix: string; + + private readonly toolName: string; + + constructor(options: ManagedConfigBlocksOptions) { + this.blockNames = options.blockNames; + this.markerPrefix = options.markerPrefix; + this.toolName = options.toolName; + } + + getConfigContent(options: GetConfigContentOptions): string { + if (!options.existingContent) return options.desiredContent; + if (this.hasManagedBlocks(options.existingContent)) { + return this.replaceManagedBlocks(options.existingContent, options.desiredContent, options.filePath); + } + return options.desiredContent; + } + + getBlock(blockName: ConfigBlockName, content: string): string { + return `${this.getStartMarker(blockName)} +${content} +${this.getEndMarker(blockName)}`; + } + + private hasManagedBlocks(content: string): boolean { + return this.blockNames.some((blockName) => content.includes(this.getStartMarker(blockName))); + } + + private replaceManagedBlocks(existingContent: string, desiredContent: string, filePath: string): string { + let content = existingContent; + for (const blockName of this.blockNames) { + const replacement = this.extractManagedBlock(desiredContent, blockName); + if (!replacement) continue; + + const nextContent = this.replaceManagedBlock(content, blockName, replacement); + if (!nextContent) { + console.warn(`Skipped updating incomplete ${blockName} block in ${this.toolName} config: ${filePath}`); + return existingContent; + } + content = nextContent; + } + return content; + } + + private extractManagedBlock(content: string, blockName: ConfigBlockName): string | undefined { + return this.getManagedBlockRegExp(blockName).exec(content)?.[0]; + } + + private replaceManagedBlock(content: string, blockName: ConfigBlockName, replacement: string): string | undefined { + const pattern = this.getManagedBlockRegExp(blockName); + if (!pattern.test(content)) return undefined; + return content.replace(pattern, replacement); + } + + private getManagedBlockRegExp(blockName: ConfigBlockName): RegExp { + return new RegExp( + `${escapeRegExp(this.getStartMarker(blockName))}[\\s\\S]*?${escapeRegExp(this.getEndMarker(blockName))}` + ); + } + + private getStartMarker(blockName: ConfigBlockName): string { + return `// wbfy:start ${this.markerPrefix}-${blockName}`; + } + + private getEndMarker(blockName: ConfigBlockName): string { + return `// wbfy:end ${this.markerPrefix}-${blockName}`; + } +} + +function escapeRegExp(value: string): string { + return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); +} diff --git a/packages/wbfy/src/generators/oxfmtConfig.ts b/packages/wbfy/src/generators/oxfmtConfig.ts index 9069daa5..12bb1a44 100644 --- a/packages/wbfy/src/generators/oxfmtConfig.ts +++ b/packages/wbfy/src/generators/oxfmtConfig.ts @@ -6,16 +6,27 @@ import type { PackageConfig } from '../packageConfig.js'; import { fsUtil } from '../utils/fsUtil.js'; import { promisePool } from '../utils/promisePool.js'; -import { generateToolConfigContent, normalizeToolConfigContent } from './toolConfigContent.js'; +import { normalizeConfigContent } from './configContent.js'; +import { ManagedConfigBlocks } from './managedConfigBlock.js'; + +const managedConfigBlocks = new ManagedConfigBlocks({ + blockNames: ['base', 'export'], + markerPrefix: 'oxfmt', + toolName: 'oxfmt', +}); export async function generateOxfmtConfig(config: PackageConfig): Promise { return logger.functionIgnoringException('generateOxfmtConfig', async () => { const legacyJsonConfigPath = path.resolve(config.dirPath, '.oxfmtrc.json'); const filePath = path.resolve(config.dirPath, 'oxfmt.config.ts'); const existingContent = await fsUtil.readFileIgnoringError(filePath); - const desiredContent = getConfigContent(config); + const desiredContent = managedConfigBlocks.getConfigContent({ + desiredContent: getConfigContent(config), + existingContent, + filePath, + }); const promises = [promisePool.run(() => fs.promises.rm(legacyJsonConfigPath, { force: true }))]; - if (normalizeToolConfigContent(existingContent) !== normalizeToolConfigContent(desiredContent)) { + if (normalizeConfigContent(existingContent) !== normalizeConfigContent(desiredContent)) { promises.push(promisePool.run(() => fsUtil.generateFile(filePath, desiredContent))); } await Promise.all(promises); @@ -23,8 +34,23 @@ export async function generateOxfmtConfig(config: PackageConfig): Promise } function getConfigContent(config: PackageConfig): string { - return generateToolConfigContent({ - isEsmPackage: config.isEsmPackage, - packageName: '@willbooster/oxfmt-config', - }); + // CommonJS packages need require/module.exports here: oxfmt config files are + // only auto-discovered as .ts, and the shared config package is ESM-only. + if (!config.isEsmPackage) { + return `${managedConfigBlocks.getBlock( + 'base', + `// oxlint-disable unicorn/prefer-module -- Oxfmt config files are only auto-discovered as .ts, and CommonJS avoids Node typeless ESM warnings. +const oxfmtConfig = require('@willbooster/oxfmt-config'); + +const config = oxfmtConfig.default ?? oxfmtConfig;` + )} + +${managedConfigBlocks.getBlock('export', 'module.exports = config;')} +`; + } + + return `${managedConfigBlocks.getBlock('base', "import config from '@willbooster/oxfmt-config';")} + +${managedConfigBlocks.getBlock('export', 'export default config;')} +`; } diff --git a/packages/wbfy/src/generators/oxlintConfig.ts b/packages/wbfy/src/generators/oxlintConfig.ts index d61f2ae6..3dffba99 100644 --- a/packages/wbfy/src/generators/oxlintConfig.ts +++ b/packages/wbfy/src/generators/oxlintConfig.ts @@ -7,9 +7,14 @@ import { fsUtil } from '../utils/fsUtil.js'; import { promisePool } from '../utils/promisePool.js'; import { isPublishedWillboosterConfigsPackage } from '../utils/willboosterConfigsUtil.js'; -import { normalizeToolConfigContent } from './toolConfigContent.js'; +import { normalizeConfigContent } from './configContent.js'; +import { ManagedConfigBlocks } from './managedConfigBlock.js'; -type OxlintBlockName = 'base' | 'export'; +const managedConfigBlocks = new ManagedConfigBlocks({ + blockNames: ['base', 'export'], + markerPrefix: 'oxlint', + toolName: 'oxlint', +}); export async function generateOxlintConfig(config: PackageConfig, _rootConfig: PackageConfig): Promise { return logger.functionIgnoringException('generateOxlintConfig', async () => { @@ -21,7 +26,11 @@ export async function generateOxlintConfig(config: PackageConfig, _rootConfig: P const desiredContent = shouldPreservePublishedLinterConfig && existingContent ? existingContent - : getConfigContentWithManagedBlocks(config, existingContent, filePath); + : managedConfigBlocks.getConfigContent({ + desiredContent: getConfigContent(config), + existingContent, + filePath, + }); const promises: Promise[] = []; if (!shouldPreservePublishedLinterConfig) { @@ -41,31 +50,20 @@ export async function generateOxlintConfig(config: PackageConfig, _rootConfig: P promisePool.run(() => fs.promises.rm(path.resolve(config.dirPath, 'eslint.config.ts'), { force: true })) ); } - if (normalizeToolConfigContent(existingContent) !== normalizeToolConfigContent(desiredContent)) { + if (normalizeConfigContent(existingContent) !== normalizeConfigContent(desiredContent)) { promises.push(promisePool.run(() => fsUtil.generateFile(filePath, desiredContent))); } await Promise.all(promises); }); } -function getConfigContentWithManagedBlocks( - config: PackageConfig, - existingContent: string | undefined, - filePath: string -): string { - const desiredContent = getConfigContent(config); - if (!existingContent) return desiredContent; - if (hasManagedBlocks(existingContent)) return replaceManagedBlocks(existingContent, desiredContent, filePath); - return desiredContent; -} - function getConfigContent(config: PackageConfig): string { // Do not collapse this to a static import for every package. CommonJS packages // type-check auto-discovered oxlint.config.ts as CommonJS, so importing the ESM // @willbooster/oxlint-config package triggers TS1479. Keep this in sync with // literacy-test's generated config pattern. if (!config.isEsmPackage) { - return `${getManagedBlock( + return `${managedConfigBlocks.getBlock( 'base', `// oxlint-disable unicorn/prefer-module -- Oxlint only auto-discovers .ts config files, and CommonJS avoids Node typeless ESM warnings. const oxlintBaseConfig = require('@willbooster/oxlint-config'); @@ -73,64 +71,12 @@ const oxlintBaseConfig = require('@willbooster/oxlint-config'); const config = oxlintBaseConfig.default ?? oxlintBaseConfig;` )} -${getManagedBlock('export', 'module.exports = config;')} +${managedConfigBlocks.getBlock('export', 'module.exports = config;')} `; } - return `${getManagedBlock('base', "import config from '@willbooster/oxlint-config';")} + return `${managedConfigBlocks.getBlock('base', "import config from '@willbooster/oxlint-config';")} -${getManagedBlock('export', 'export default config;')} +${managedConfigBlocks.getBlock('export', 'export default config;')} `; } - -function hasManagedBlocks(content: string): boolean { - return content.includes(getStartMarker('base')) || content.includes(getStartMarker('export')); -} - -function replaceManagedBlocks(existingContent: string, desiredContent: string, filePath: string): string { - let content = existingContent; - for (const blockName of ['base', 'export'] satisfies OxlintBlockName[]) { - const replacement = extractManagedBlock(desiredContent, blockName); - if (!replacement) continue; - - const nextContent = replaceManagedBlock(content, blockName, replacement); - if (!nextContent) { - console.warn(`Skipped updating incomplete ${blockName} block in oxlint config: ${filePath}`); - return existingContent; - } - content = nextContent; - } - return content; -} - -function extractManagedBlock(content: string, blockName: OxlintBlockName): string | undefined { - return getManagedBlockRegExp(blockName).exec(content)?.[0]; -} - -function replaceManagedBlock(content: string, blockName: OxlintBlockName, replacement: string): string | undefined { - const pattern = getManagedBlockRegExp(blockName); - if (!pattern.test(content)) return undefined; - return content.replace(pattern, replacement); -} - -function getManagedBlockRegExp(blockName: OxlintBlockName): RegExp { - return new RegExp(`${escapeRegExp(getStartMarker(blockName))}[\\s\\S]*?${escapeRegExp(getEndMarker(blockName))}`); -} - -function getManagedBlock(blockName: OxlintBlockName, content: string): string { - return `${getStartMarker(blockName)} -${content} -${getEndMarker(blockName)}`; -} - -function getStartMarker(blockName: OxlintBlockName): string { - return `// wbfy:start oxlint-${blockName}`; -} - -function getEndMarker(blockName: OxlintBlockName): string { - return `// wbfy:end oxlint-${blockName}`; -} - -function escapeRegExp(value: string): string { - return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); -} diff --git a/packages/wbfy/src/generators/toolConfigContent.ts b/packages/wbfy/src/generators/toolConfigContent.ts deleted file mode 100644 index 26a90800..00000000 --- a/packages/wbfy/src/generators/toolConfigContent.ts +++ /dev/null @@ -1,26 +0,0 @@ -interface ToolConfigContentOptions { - isEsmPackage: boolean; - packageName: string; -} - -export function generateToolConfigContent(options: ToolConfigContentOptions): string { - // CommonJS packages need require/module.exports here: these .ts config files are - // auto-discovered and type-checked as CommonJS, but the shared config packages - // are ESM-only. - if (!options.isEsmPackage) { - return `// oxlint-disable unicorn/prefer-module -- Tool config files are auto-discovered as .ts, and CommonJS avoids Node typeless ESM warnings. -const toolConfig = require('${options.packageName}'); - -module.exports = toolConfig.default ?? toolConfig; -`; - } - - return `import config from '${options.packageName}'; - -export default config; -`; -} - -export function normalizeToolConfigContent(content: string | undefined): string | undefined { - return content?.trim(); -}