Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions oxfmt.config.ts
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions packages/shared-lib-blitz-next/oxfmt.config.ts
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions packages/shared-lib-next/oxfmt.config.ts
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions packages/shared-lib-node/oxfmt.config.ts
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions packages/shared-lib-react/oxfmt.config.ts
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions packages/shared-lib/oxfmt.config.ts
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions packages/wb/oxfmt.config.ts
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions packages/wbfy/oxfmt.config.ts
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions packages/wbfy/src/generators/configContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function normalizeConfigContent(content: string | undefined): string | undefined {
return content?.trim();
}
89 changes: 89 additions & 0 deletions packages/wbfy/src/generators/managedConfigBlock.ts
Original file line number Diff line number Diff line change
@@ -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`\$&`);
}
40 changes: 33 additions & 7 deletions packages/wbfy/src/generators/oxfmtConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,51 @@ 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<void> {
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);
});
}

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;')}
`;
}
88 changes: 17 additions & 71 deletions packages/wbfy/src/generators/oxlintConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
return logger.functionIgnoringException('generateOxlintConfig', async () => {
Expand All @@ -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<void>[] = [];
if (!shouldPreservePublishedLinterConfig) {
Expand All @@ -41,96 +50,33 @@ 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');

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`\$&`);
}
26 changes: 0 additions & 26 deletions packages/wbfy/src/generators/toolConfigContent.ts

This file was deleted.

Loading