From a540729afa7ec333d0f7d9de0bd93ec6a3833dad Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 4 May 2026 13:52:07 +0900 Subject: [PATCH 1/5] fix: clarify oxfmt config content generation Co-authored-by: WillBooster (Codex CLI) --- packages/wbfy/src/generators/configContent.ts | 3 +++ packages/wbfy/src/generators/oxfmtConfig.ts | 8 +++--- .../wbfy/src/generators/oxfmtConfigContent.ts | 20 ++++++++++++++ packages/wbfy/src/generators/oxlintConfig.ts | 4 +-- .../wbfy/src/generators/toolConfigContent.ts | 26 ------------------- 5 files changed, 29 insertions(+), 32 deletions(-) create mode 100644 packages/wbfy/src/generators/configContent.ts create mode 100644 packages/wbfy/src/generators/oxfmtConfigContent.ts delete mode 100644 packages/wbfy/src/generators/toolConfigContent.ts 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/oxfmtConfig.ts b/packages/wbfy/src/generators/oxfmtConfig.ts index 9069daa5..09bbd372 100644 --- a/packages/wbfy/src/generators/oxfmtConfig.ts +++ b/packages/wbfy/src/generators/oxfmtConfig.ts @@ -6,7 +6,8 @@ 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 { generateOxfmtConfigContent } from './oxfmtConfigContent.js'; export async function generateOxfmtConfig(config: PackageConfig): Promise { return logger.functionIgnoringException('generateOxfmtConfig', async () => { @@ -15,7 +16,7 @@ export async function generateOxfmtConfig(config: PackageConfig): Promise const existingContent = await fsUtil.readFileIgnoringError(filePath); const desiredContent = getConfigContent(config); 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 +24,7 @@ export async function generateOxfmtConfig(config: PackageConfig): Promise } function getConfigContent(config: PackageConfig): string { - return generateToolConfigContent({ + return generateOxfmtConfigContent({ isEsmPackage: config.isEsmPackage, - packageName: '@willbooster/oxfmt-config', }); } diff --git a/packages/wbfy/src/generators/oxfmtConfigContent.ts b/packages/wbfy/src/generators/oxfmtConfigContent.ts new file mode 100644 index 00000000..aeb3184d --- /dev/null +++ b/packages/wbfy/src/generators/oxfmtConfigContent.ts @@ -0,0 +1,20 @@ +interface OxfmtConfigContentOptions { + isEsmPackage: boolean; +} + +export function generateOxfmtConfigContent(options: OxfmtConfigContentOptions): string { + // 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 (!options.isEsmPackage) { + return `// 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'); + +module.exports = oxfmtConfig.default ?? oxfmtConfig; +`; + } + + return `import config from '@willbooster/oxfmt-config'; + +export default config; +`; +} diff --git a/packages/wbfy/src/generators/oxlintConfig.ts b/packages/wbfy/src/generators/oxlintConfig.ts index d61f2ae6..bd5a9b05 100644 --- a/packages/wbfy/src/generators/oxlintConfig.ts +++ b/packages/wbfy/src/generators/oxlintConfig.ts @@ -7,7 +7,7 @@ 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'; type OxlintBlockName = 'base' | 'export'; @@ -41,7 +41,7 @@ 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); 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(); -} From bc34e2fb56f439ed23c863ca62c0115e33ed30b2 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 4 May 2026 13:53:17 +0900 Subject: [PATCH 2/5] fix: keep oxfmt config content inline Co-authored-by: WillBooster (Codex CLI) --- packages/wbfy/src/generators/oxfmtConfig.ts | 18 +++++++++++++---- .../wbfy/src/generators/oxfmtConfigContent.ts | 20 ------------------- 2 files changed, 14 insertions(+), 24 deletions(-) delete mode 100644 packages/wbfy/src/generators/oxfmtConfigContent.ts diff --git a/packages/wbfy/src/generators/oxfmtConfig.ts b/packages/wbfy/src/generators/oxfmtConfig.ts index 09bbd372..f2e1b7bd 100644 --- a/packages/wbfy/src/generators/oxfmtConfig.ts +++ b/packages/wbfy/src/generators/oxfmtConfig.ts @@ -7,7 +7,6 @@ import { fsUtil } from '../utils/fsUtil.js'; import { promisePool } from '../utils/promisePool.js'; import { normalizeConfigContent } from './configContent.js'; -import { generateOxfmtConfigContent } from './oxfmtConfigContent.js'; export async function generateOxfmtConfig(config: PackageConfig): Promise { return logger.functionIgnoringException('generateOxfmtConfig', async () => { @@ -24,7 +23,18 @@ export async function generateOxfmtConfig(config: PackageConfig): Promise } function getConfigContent(config: PackageConfig): string { - return generateOxfmtConfigContent({ - isEsmPackage: config.isEsmPackage, - }); + // 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 `// 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'); + +module.exports = oxfmtConfig.default ?? oxfmtConfig; +`; + } + + return `import config from '@willbooster/oxfmt-config'; + +export default config; +`; } diff --git a/packages/wbfy/src/generators/oxfmtConfigContent.ts b/packages/wbfy/src/generators/oxfmtConfigContent.ts deleted file mode 100644 index aeb3184d..00000000 --- a/packages/wbfy/src/generators/oxfmtConfigContent.ts +++ /dev/null @@ -1,20 +0,0 @@ -interface OxfmtConfigContentOptions { - isEsmPackage: boolean; -} - -export function generateOxfmtConfigContent(options: OxfmtConfigContentOptions): string { - // 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 (!options.isEsmPackage) { - return `// 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'); - -module.exports = oxfmtConfig.default ?? oxfmtConfig; -`; - } - - return `import config from '@willbooster/oxfmt-config'; - -export default config; -`; -} From 920d0c17a069ef5f1acfbee9461001814586b32c Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 4 May 2026 13:55:43 +0900 Subject: [PATCH 3/5] fix: add managed markers to oxfmt config Co-authored-by: WillBooster (Codex CLI) --- oxfmt.config.ts | 4 + .../shared-lib-blitz-next/oxfmt.config.ts | 4 + packages/shared-lib-next/oxfmt.config.ts | 4 + packages/shared-lib-node/oxfmt.config.ts | 4 + packages/shared-lib-react/oxfmt.config.ts | 4 + packages/shared-lib/oxfmt.config.ts | 4 + packages/wb/oxfmt.config.ts | 4 + packages/wbfy/oxfmt.config.ts | 4 + packages/wbfy/src/generators/oxfmtConfig.ts | 80 +++++++++++++++++-- 9 files changed, 107 insertions(+), 5 deletions(-) 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/oxfmtConfig.ts b/packages/wbfy/src/generators/oxfmtConfig.ts index f2e1b7bd..74c2e50d 100644 --- a/packages/wbfy/src/generators/oxfmtConfig.ts +++ b/packages/wbfy/src/generators/oxfmtConfig.ts @@ -8,12 +8,14 @@ import { promisePool } from '../utils/promisePool.js'; import { normalizeConfigContent } from './configContent.js'; +type OxfmtBlockName = 'base' | 'export'; + 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 = getConfigContentWithManagedBlocks(config, existingContent, filePath); const promises = [promisePool.run(() => fs.promises.rm(legacyJsonConfigPath, { force: true }))]; if (normalizeConfigContent(existingContent) !== normalizeConfigContent(desiredContent)) { promises.push(promisePool.run(() => fsUtil.generateFile(filePath, desiredContent))); @@ -22,19 +24,87 @@ export async function generateOxfmtConfig(config: PackageConfig): Promise }); } +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 { // 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 `// oxlint-disable unicorn/prefer-module -- Oxfmt config files are only auto-discovered as .ts, and CommonJS avoids Node typeless ESM warnings. + return `${getManagedBlock( + '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'); -module.exports = oxfmtConfig.default ?? oxfmtConfig; +const config = oxfmtConfig.default ?? oxfmtConfig;` + )} + +${getManagedBlock('export', 'module.exports = config;')} `; } - return `import config from '@willbooster/oxfmt-config'; + return `${getManagedBlock('base', "import config from '@willbooster/oxfmt-config';")} -export default config; +${getManagedBlock('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 OxfmtBlockName[]) { + const replacement = extractManagedBlock(desiredContent, blockName); + if (!replacement) continue; + + const nextContent = replaceManagedBlock(content, blockName, replacement); + if (!nextContent) { + console.warn(`Skipped updating incomplete ${blockName} block in oxfmt config: ${filePath}`); + return existingContent; + } + content = nextContent; + } + return content; +} + +function extractManagedBlock(content: string, blockName: OxfmtBlockName): string | undefined { + return getManagedBlockRegExp(blockName).exec(content)?.[0]; +} + +function replaceManagedBlock(content: string, blockName: OxfmtBlockName, replacement: string): string | undefined { + const pattern = getManagedBlockRegExp(blockName); + if (!pattern.test(content)) return undefined; + return content.replace(pattern, replacement); +} + +function getManagedBlockRegExp(blockName: OxfmtBlockName): RegExp { + return new RegExp(`${escapeRegExp(getStartMarker(blockName))}[\\s\\S]*?${escapeRegExp(getEndMarker(blockName))}`); +} + +function getManagedBlock(blockName: OxfmtBlockName, content: string): string { + return `${getStartMarker(blockName)} +${content} +${getEndMarker(blockName)}`; +} + +function getStartMarker(blockName: OxfmtBlockName): string { + return `// wbfy:start oxfmt-${blockName}`; +} + +function getEndMarker(blockName: OxfmtBlockName): string { + return `// wbfy:end oxfmt-${blockName}`; +} + +function escapeRegExp(value: string): string { + return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); +} From e3bf436e76980a58d05c82531de36d0e8f8f05f0 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 4 May 2026 14:00:02 +0900 Subject: [PATCH 4/5] fix: share managed config block helpers Co-authored-by: WillBooster (Codex CLI) --- .../wbfy/src/generators/managedConfigBlock.ts | 98 +++++++++++++++++++ packages/wbfy/src/generators/oxfmtConfig.ts | 86 ++++------------ packages/wbfy/src/generators/oxlintConfig.ts | 86 ++++------------ 3 files changed, 132 insertions(+), 138 deletions(-) create mode 100644 packages/wbfy/src/generators/managedConfigBlock.ts diff --git a/packages/wbfy/src/generators/managedConfigBlock.ts b/packages/wbfy/src/generators/managedConfigBlock.ts new file mode 100644 index 00000000..c4d3a7ad --- /dev/null +++ b/packages/wbfy/src/generators/managedConfigBlock.ts @@ -0,0 +1,98 @@ +export type ConfigBlockName = 'base' | 'export'; + +interface GetConfigContentWithManagedBlocksOptions { + blockNames: readonly ConfigBlockName[]; + desiredContent: string; + existingContent: string | undefined; + filePath: string; + markerPrefix: string; + toolName: string; +} + +export function getConfigContentWithManagedBlocks(options: GetConfigContentWithManagedBlocksOptions): string { + if (!options.existingContent) return options.desiredContent; + if (hasManagedBlocks(options.existingContent, options)) { + return replaceManagedBlocks(options.existingContent, options.desiredContent, options); + } + return options.desiredContent; +} + +export function getManagedBlock( + blockName: ConfigBlockName, + content: string, + options: Pick +): string { + return `${getStartMarker(blockName, options)} +${content} +${getEndMarker(blockName, options)}`; +} + +function hasManagedBlocks(content: string, options: GetConfigContentWithManagedBlocksOptions): boolean { + return options.blockNames.some((blockName) => content.includes(getStartMarker(blockName, options))); +} + +function replaceManagedBlocks( + existingContent: string, + desiredContent: string, + options: GetConfigContentWithManagedBlocksOptions +): string { + let content = existingContent; + for (const blockName of options.blockNames) { + const replacement = extractManagedBlock(desiredContent, blockName, options); + if (!replacement) continue; + + const nextContent = replaceManagedBlock(content, blockName, replacement, options); + if (!nextContent) { + console.warn(`Skipped updating incomplete ${blockName} block in ${options.toolName} config: ${options.filePath}`); + return existingContent; + } + content = nextContent; + } + return content; +} + +function extractManagedBlock( + content: string, + blockName: ConfigBlockName, + options: Pick +): string | undefined { + return getManagedBlockRegExp(blockName, options).exec(content)?.[0]; +} + +function replaceManagedBlock( + content: string, + blockName: ConfigBlockName, + replacement: string, + options: Pick +): string | undefined { + const pattern = getManagedBlockRegExp(blockName, options); + if (!pattern.test(content)) return undefined; + return content.replace(pattern, replacement); +} + +function getManagedBlockRegExp( + blockName: ConfigBlockName, + options: Pick +): RegExp { + return new RegExp( + `${escapeRegExp(getStartMarker(blockName, options))}[\\s\\S]*?${escapeRegExp(getEndMarker(blockName, options))}` + ); +} + +function getStartMarker( + blockName: ConfigBlockName, + options: Pick +): string { + return `// wbfy:start ${options.markerPrefix}-${blockName}`; +} + +function getEndMarker( + blockName: ConfigBlockName, + options: Pick +): string { + return `// wbfy:end ${options.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 74c2e50d..c9d76bed 100644 --- a/packages/wbfy/src/generators/oxfmtConfig.ts +++ b/packages/wbfy/src/generators/oxfmtConfig.ts @@ -7,15 +7,25 @@ import { fsUtil } from '../utils/fsUtil.js'; import { promisePool } from '../utils/promisePool.js'; import { normalizeConfigContent } from './configContent.js'; +import { getConfigContentWithManagedBlocks, getManagedBlock } from './managedConfigBlock.js'; -type OxfmtBlockName = 'base' | 'export'; +const managedBlockOptions = { + blockNames: ['base', 'export'], + markerPrefix: 'oxfmt', + toolName: 'oxfmt', +} as const; 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 = getConfigContentWithManagedBlocks(config, existingContent, filePath); + const desiredContent = getConfigContentWithManagedBlocks({ + ...managedBlockOptions, + desiredContent: getConfigContent(config), + existingContent, + filePath, + }); const promises = [promisePool.run(() => fs.promises.rm(legacyJsonConfigPath, { force: true }))]; if (normalizeConfigContent(existingContent) !== normalizeConfigContent(desiredContent)) { promises.push(promisePool.run(() => fsUtil.generateFile(filePath, desiredContent))); @@ -24,17 +34,6 @@ export async function generateOxfmtConfig(config: PackageConfig): Promise }); } -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 { // CommonJS packages need require/module.exports here: oxfmt config files are // only auto-discovered as .ts, and the shared config package is ESM-only. @@ -44,67 +43,16 @@ function getConfigContent(config: PackageConfig): string { `// 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;` +const config = oxfmtConfig.default ?? oxfmtConfig;`, + managedBlockOptions )} -${getManagedBlock('export', 'module.exports = config;')} +${getManagedBlock('export', 'module.exports = config;', managedBlockOptions)} `; } - return `${getManagedBlock('base', "import config from '@willbooster/oxfmt-config';")} + return `${getManagedBlock('base', "import config from '@willbooster/oxfmt-config';", managedBlockOptions)} -${getManagedBlock('export', 'export default config;')} +${getManagedBlock('export', 'export default config;', managedBlockOptions)} `; } - -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 OxfmtBlockName[]) { - const replacement = extractManagedBlock(desiredContent, blockName); - if (!replacement) continue; - - const nextContent = replaceManagedBlock(content, blockName, replacement); - if (!nextContent) { - console.warn(`Skipped updating incomplete ${blockName} block in oxfmt config: ${filePath}`); - return existingContent; - } - content = nextContent; - } - return content; -} - -function extractManagedBlock(content: string, blockName: OxfmtBlockName): string | undefined { - return getManagedBlockRegExp(blockName).exec(content)?.[0]; -} - -function replaceManagedBlock(content: string, blockName: OxfmtBlockName, replacement: string): string | undefined { - const pattern = getManagedBlockRegExp(blockName); - if (!pattern.test(content)) return undefined; - return content.replace(pattern, replacement); -} - -function getManagedBlockRegExp(blockName: OxfmtBlockName): RegExp { - return new RegExp(`${escapeRegExp(getStartMarker(blockName))}[\\s\\S]*?${escapeRegExp(getEndMarker(blockName))}`); -} - -function getManagedBlock(blockName: OxfmtBlockName, content: string): string { - return `${getStartMarker(blockName)} -${content} -${getEndMarker(blockName)}`; -} - -function getStartMarker(blockName: OxfmtBlockName): string { - return `// wbfy:start oxfmt-${blockName}`; -} - -function getEndMarker(blockName: OxfmtBlockName): string { - return `// wbfy:end oxfmt-${blockName}`; -} - -function escapeRegExp(value: string): string { - return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); -} diff --git a/packages/wbfy/src/generators/oxlintConfig.ts b/packages/wbfy/src/generators/oxlintConfig.ts index bd5a9b05..84654359 100644 --- a/packages/wbfy/src/generators/oxlintConfig.ts +++ b/packages/wbfy/src/generators/oxlintConfig.ts @@ -8,8 +8,13 @@ import { promisePool } from '../utils/promisePool.js'; import { isPublishedWillboosterConfigsPackage } from '../utils/willboosterConfigsUtil.js'; import { normalizeConfigContent } from './configContent.js'; +import { getConfigContentWithManagedBlocks, getManagedBlock } from './managedConfigBlock.js'; -type OxlintBlockName = 'base' | 'export'; +const managedBlockOptions = { + blockNames: ['base', 'export'], + markerPrefix: 'oxlint', + toolName: 'oxlint', +} as const; export async function generateOxlintConfig(config: PackageConfig, _rootConfig: PackageConfig): Promise { return logger.functionIgnoringException('generateOxlintConfig', async () => { @@ -21,7 +26,12 @@ export async function generateOxlintConfig(config: PackageConfig, _rootConfig: P const desiredContent = shouldPreservePublishedLinterConfig && existingContent ? existingContent - : getConfigContentWithManagedBlocks(config, existingContent, filePath); + : getConfigContentWithManagedBlocks({ + ...managedBlockOptions, + desiredContent: getConfigContent(config), + existingContent, + filePath, + }); const promises: Promise[] = []; if (!shouldPreservePublishedLinterConfig) { @@ -48,17 +58,6 @@ export async function generateOxlintConfig(config: PackageConfig, _rootConfig: P }); } -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 @@ -70,67 +69,16 @@ function getConfigContent(config: PackageConfig): string { `// 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'); -const config = oxlintBaseConfig.default ?? oxlintBaseConfig;` +const config = oxlintBaseConfig.default ?? oxlintBaseConfig;`, + managedBlockOptions )} -${getManagedBlock('export', 'module.exports = config;')} +${getManagedBlock('export', 'module.exports = config;', managedBlockOptions)} `; } - return `${getManagedBlock('base', "import config from '@willbooster/oxlint-config';")} + return `${getManagedBlock('base', "import config from '@willbooster/oxlint-config';", managedBlockOptions)} -${getManagedBlock('export', 'export default config;')} +${getManagedBlock('export', 'export default config;', managedBlockOptions)} `; } - -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`\$&`); -} From 18562dfe1b852a71eaea84ed9244eea8d275b2be Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 4 May 2026 14:02:03 +0900 Subject: [PATCH 5/5] fix: encapsulate managed config block state Co-authored-by: WillBooster (Codex CLI) --- .../wbfy/src/generators/managedConfigBlock.ts | 137 ++++++++---------- packages/wbfy/src/generators/oxfmtConfig.ts | 20 ++- packages/wbfy/src/generators/oxlintConfig.ts | 20 ++- 3 files changed, 82 insertions(+), 95 deletions(-) diff --git a/packages/wbfy/src/generators/managedConfigBlock.ts b/packages/wbfy/src/generators/managedConfigBlock.ts index c4d3a7ad..77524d4a 100644 --- a/packages/wbfy/src/generators/managedConfigBlock.ts +++ b/packages/wbfy/src/generators/managedConfigBlock.ts @@ -1,96 +1,87 @@ export type ConfigBlockName = 'base' | 'export'; -interface GetConfigContentWithManagedBlocksOptions { +interface ManagedConfigBlocksOptions { blockNames: readonly ConfigBlockName[]; + markerPrefix: string; + toolName: string; +} + +interface GetConfigContentOptions { desiredContent: string; existingContent: string | undefined; filePath: string; - markerPrefix: string; - toolName: string; } -export function getConfigContentWithManagedBlocks(options: GetConfigContentWithManagedBlocksOptions): string { - if (!options.existingContent) return options.desiredContent; - if (hasManagedBlocks(options.existingContent, options)) { - return replaceManagedBlocks(options.existingContent, options.desiredContent, options); +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; } - return options.desiredContent; -} -export function getManagedBlock( - blockName: ConfigBlockName, - content: string, - options: Pick -): string { - return `${getStartMarker(blockName, options)} + 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} -${getEndMarker(blockName, options)}`; -} +${this.getEndMarker(blockName)}`; + } -function hasManagedBlocks(content: string, options: GetConfigContentWithManagedBlocksOptions): boolean { - return options.blockNames.some((blockName) => content.includes(getStartMarker(blockName, options))); -} + 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; -function replaceManagedBlocks( - existingContent: string, - desiredContent: string, - options: GetConfigContentWithManagedBlocksOptions -): string { - let content = existingContent; - for (const blockName of options.blockNames) { - const replacement = extractManagedBlock(desiredContent, blockName, options); - if (!replacement) continue; - - const nextContent = replaceManagedBlock(content, blockName, replacement, options); - if (!nextContent) { - console.warn(`Skipped updating incomplete ${blockName} block in ${options.toolName} config: ${options.filePath}`); - return existingContent; + 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; } - content = nextContent; + return content; } - return content; -} -function extractManagedBlock( - content: string, - blockName: ConfigBlockName, - options: Pick -): string | undefined { - return getManagedBlockRegExp(blockName, options).exec(content)?.[0]; -} + private extractManagedBlock(content: string, blockName: ConfigBlockName): string | undefined { + return this.getManagedBlockRegExp(blockName).exec(content)?.[0]; + } -function replaceManagedBlock( - content: string, - blockName: ConfigBlockName, - replacement: string, - options: Pick -): string | undefined { - const pattern = getManagedBlockRegExp(blockName, options); - if (!pattern.test(content)) return undefined; - return content.replace(pattern, replacement); -} + 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); + } -function getManagedBlockRegExp( - blockName: ConfigBlockName, - options: Pick -): RegExp { - return new RegExp( - `${escapeRegExp(getStartMarker(blockName, options))}[\\s\\S]*?${escapeRegExp(getEndMarker(blockName, options))}` - ); -} + private getManagedBlockRegExp(blockName: ConfigBlockName): RegExp { + return new RegExp( + `${escapeRegExp(this.getStartMarker(blockName))}[\\s\\S]*?${escapeRegExp(this.getEndMarker(blockName))}` + ); + } -function getStartMarker( - blockName: ConfigBlockName, - options: Pick -): string { - return `// wbfy:start ${options.markerPrefix}-${blockName}`; -} + private getStartMarker(blockName: ConfigBlockName): string { + return `// wbfy:start ${this.markerPrefix}-${blockName}`; + } -function getEndMarker( - blockName: ConfigBlockName, - options: Pick -): string { - return `// wbfy:end ${options.markerPrefix}-${blockName}`; + private getEndMarker(blockName: ConfigBlockName): string { + return `// wbfy:end ${this.markerPrefix}-${blockName}`; + } } function escapeRegExp(value: string): string { diff --git a/packages/wbfy/src/generators/oxfmtConfig.ts b/packages/wbfy/src/generators/oxfmtConfig.ts index c9d76bed..12bb1a44 100644 --- a/packages/wbfy/src/generators/oxfmtConfig.ts +++ b/packages/wbfy/src/generators/oxfmtConfig.ts @@ -7,21 +7,20 @@ import { fsUtil } from '../utils/fsUtil.js'; import { promisePool } from '../utils/promisePool.js'; import { normalizeConfigContent } from './configContent.js'; -import { getConfigContentWithManagedBlocks, getManagedBlock } from './managedConfigBlock.js'; +import { ManagedConfigBlocks } from './managedConfigBlock.js'; -const managedBlockOptions = { +const managedConfigBlocks = new ManagedConfigBlocks({ blockNames: ['base', 'export'], markerPrefix: 'oxfmt', toolName: 'oxfmt', -} as const; +}); 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 = getConfigContentWithManagedBlocks({ - ...managedBlockOptions, + const desiredContent = managedConfigBlocks.getConfigContent({ desiredContent: getConfigContent(config), existingContent, filePath, @@ -38,21 +37,20 @@ function getConfigContent(config: PackageConfig): string { // 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 `${getManagedBlock( + 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;`, - managedBlockOptions +const config = oxfmtConfig.default ?? oxfmtConfig;` )} -${getManagedBlock('export', 'module.exports = config;', managedBlockOptions)} +${managedConfigBlocks.getBlock('export', 'module.exports = config;')} `; } - return `${getManagedBlock('base', "import config from '@willbooster/oxfmt-config';", managedBlockOptions)} + return `${managedConfigBlocks.getBlock('base', "import config from '@willbooster/oxfmt-config';")} -${getManagedBlock('export', 'export default config;', managedBlockOptions)} +${managedConfigBlocks.getBlock('export', 'export default config;')} `; } diff --git a/packages/wbfy/src/generators/oxlintConfig.ts b/packages/wbfy/src/generators/oxlintConfig.ts index 84654359..3dffba99 100644 --- a/packages/wbfy/src/generators/oxlintConfig.ts +++ b/packages/wbfy/src/generators/oxlintConfig.ts @@ -8,13 +8,13 @@ import { promisePool } from '../utils/promisePool.js'; import { isPublishedWillboosterConfigsPackage } from '../utils/willboosterConfigsUtil.js'; import { normalizeConfigContent } from './configContent.js'; -import { getConfigContentWithManagedBlocks, getManagedBlock } from './managedConfigBlock.js'; +import { ManagedConfigBlocks } from './managedConfigBlock.js'; -const managedBlockOptions = { +const managedConfigBlocks = new ManagedConfigBlocks({ blockNames: ['base', 'export'], markerPrefix: 'oxlint', toolName: 'oxlint', -} as const; +}); export async function generateOxlintConfig(config: PackageConfig, _rootConfig: PackageConfig): Promise { return logger.functionIgnoringException('generateOxlintConfig', async () => { @@ -26,8 +26,7 @@ export async function generateOxlintConfig(config: PackageConfig, _rootConfig: P const desiredContent = shouldPreservePublishedLinterConfig && existingContent ? existingContent - : getConfigContentWithManagedBlocks({ - ...managedBlockOptions, + : managedConfigBlocks.getConfigContent({ desiredContent: getConfigContent(config), existingContent, filePath, @@ -64,21 +63,20 @@ function getConfigContent(config: PackageConfig): string { // @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'); -const config = oxlintBaseConfig.default ?? oxlintBaseConfig;`, - managedBlockOptions +const config = oxlintBaseConfig.default ?? oxlintBaseConfig;` )} -${getManagedBlock('export', 'module.exports = config;', managedBlockOptions)} +${managedConfigBlocks.getBlock('export', 'module.exports = config;')} `; } - return `${getManagedBlock('base', "import config from '@willbooster/oxlint-config';", managedBlockOptions)} + return `${managedConfigBlocks.getBlock('base', "import config from '@willbooster/oxlint-config';")} -${getManagedBlock('export', 'export default config;', managedBlockOptions)} +${managedConfigBlocks.getBlock('export', 'export default config;')} `; }