Skip to content

Commit 3fd4115

Browse files
committed
feat!: add code block rule and mdx formatters using oxfmt
Signed-off-by: Ryan Bower <rbower@qti.qualcomm.com>
1 parent 2e063eb commit 3fd4115

22 files changed

Lines changed: 1240 additions & 40 deletions

packages/configs/eslint-config-mdx/.remarkrc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
"remark-lint-no-consecutive-blank-lines",
1111
"remark-lint-no-duplicate-headings-in-section",
1212
["remark-lint-no-heading-punctuation", ",.:;"],
13-
["remark-lint-unordered-list-marker-style", "-"]
13+
["remark-lint-unordered-list-marker-style", "-"],
14+
"./dist/remark-lint-code-format.js",
15+
"./dist/remark-lint-mdx-jsx-format.js",
16+
"./dist/remark-preserve-alert-markers.js"
1417
],
1518
"settings": {
1619
"bullet": "-",

packages/configs/eslint-config-mdx/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Apr 19th, 2026
2222

2323
### Miscellaneous Chores
2424

25-
- upgrade prettier and eslint dependenies, fix resulting formatting issues ([332185a](https://github.com/qualcomm/qualcomm-ui/commit/332185a))
25+
- upgrade prettier and eslint dependencies, fix resulting formatting issues ([332185a](https://github.com/qualcomm/qualcomm-ui/commit/332185a))
2626

2727
## 1.1.0
2828

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type {BuildOptions} from "esbuild"
2+
3+
import {buildOrWatch, hasArg, logPlugin} from "@qualcomm-ui/esbuild"
4+
5+
import pkg from "./package.json"
6+
7+
async function build(argv: string[]) {
8+
const IS_WATCH = hasArg(argv, "--watch")
9+
10+
const buildOpts: BuildOptions = {
11+
bundle: true,
12+
entryPoints: [
13+
"./src/index.ts",
14+
"./src/remark-lint-code-format.ts",
15+
"./src/remark-lint-mdx-jsx-format.ts",
16+
"./src/remark-preserve-alert-markers.ts",
17+
"./src/remarkrc.ts",
18+
],
19+
external: [
20+
...Object.keys(pkg.devDependencies ?? {}),
21+
...Object.keys(pkg.peerDependencies ?? {}),
22+
],
23+
format: "esm",
24+
metafile: true,
25+
outdir: "./dist",
26+
platform: "node",
27+
plugins: [logPlugin({bundleSizeOptions: {logMode: "all"}})],
28+
sourcemap: true,
29+
target: "es2022",
30+
tsconfig: "tsconfig.lib.json",
31+
}
32+
33+
await buildOrWatch(buildOpts, IS_WATCH)
34+
}
35+
36+
build(process.argv)

packages/configs/eslint-config-mdx/index.d.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

packages/configs/eslint-config-mdx/index.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

packages/configs/eslint-config-mdx/package.json

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,73 @@
88
"publishConfig": {
99
"access": "public"
1010
},
11+
"keywords": [
12+
"config",
13+
"eslint",
14+
"react"
15+
],
1116
"type": "module",
1217
"sideEffects": false,
1318
"files": [
14-
"index.d.ts",
15-
"index.js",
16-
"recommended.js",
19+
"dist/",
1720
".remarkrc"
1821
],
22+
"main": "./dist/index.js",
23+
"module": "./dist/index.js",
24+
"types": "./dist/index.d.ts",
1925
"exports": {
2026
".": {
21-
"types": "./index.d.ts",
22-
"import": "./index.js",
23-
"default": "./index.js"
27+
"types": "./dist/index.d.ts",
28+
"import": "./dist/index.js",
29+
"default": "./dist/index.js"
2430
},
2531
"./remarkrc": {
26-
"types": "./remarkrc.d.ts",
27-
"import": "./remarkrc.js",
28-
"default": "./remarkrc.js"
32+
"types": "./dist/remarkrc.d.ts",
33+
"import": "./dist/remarkrc.js",
34+
"default": "./dist/remarkrc.js"
35+
},
36+
"./remark-lint-code-format": {
37+
"types": "./dist/remark-lint-code-format.d.ts",
38+
"import": "./dist/remark-lint-code-format.js",
39+
"default": "./dist/remark-lint-code-format.js"
40+
},
41+
"./remark-lint-mdx-jsx-format": {
42+
"types": "./dist/remark-lint-mdx-jsx-format.d.ts",
43+
"import": "./dist/remark-lint-mdx-jsx-format.js",
44+
"default": "./dist/remark-lint-mdx-jsx-format.js"
45+
},
46+
"./remark-preserve-alert-markers": {
47+
"types": "./dist/remark-preserve-alert-markers.d.ts",
48+
"import": "./dist/remark-preserve-alert-markers.js",
49+
"default": "./dist/remark-preserve-alert-markers.js"
2950
}
3051
},
3152
"scripts": {
32-
"prepublishOnly": "qui-cli pre-publish",
53+
"build": "run-s clean build:js build:types",
54+
"build:js": "tsx build.ts",
55+
"build:types": "tsc -p tsconfig.lib.json",
56+
"build:watch": "run-p watch:js watch:types",
57+
"watch:js": "tsx build.ts --watch",
58+
"watch:types": "tsc -p tsconfig.lib.json -w --preserveWatchOutput",
59+
"clean": "shx rm -rf dist",
60+
"lint": "eslint --quiet",
61+
"test": "turbo build --filter @qualcomm-ui/eslint-config-mdx && vitest -c vitest.config.ts --run",
62+
"test:unit:ci": "pnpm build && cross-env NODE_OPTIONS='' vitest -c vitest.config.ts --run --pool=threads --no-file-parallelism",
63+
"prepublishOnly": "pnpm build && qui-cli pre-publish",
3364
"postpublish": "qui-cli post-publish"
3465
},
3566
"devDependencies": {
3667
"@eslint/core": "^1.0.0",
3768
"@qualcomm-ui/cli": "catalog:",
69+
"@qualcomm-ui/esbuild": "catalog:",
70+
"@qualcomm-ui/tsconfig": "catalog:",
71+
"cross-env": "^10.1.0",
72+
"esbuild": "catalog:",
3873
"eslint-mdx": "^3.7.0",
3974
"eslint-plugin-mdx": "^3.7.0",
4075
"eslint-plugin-react": "^7.37.5",
76+
"npm-run-all2": "catalog:",
77+
"oxfmt": "catalog:",
4178
"remark-frontmatter": "^5.0.0",
4279
"remark-lint-fenced-code-flag": "^4.2.0",
4380
"remark-lint-final-newline": "^3.0.1",
@@ -49,13 +86,18 @@
4986
"remark-lint-no-duplicate-headings-in-section": "^4.0.1",
5087
"remark-lint-no-heading-punctuation": "^4.0.1",
5188
"remark-lint-unordered-list-marker-style": "^4.0.1",
52-
"unified": "catalog:"
89+
"shx": "catalog:",
90+
"tsx": "catalog:",
91+
"typescript": "catalog:",
92+
"unified": "catalog:",
93+
"vitest": "catalog:"
5394
},
5495
"peerDependencies": {
5596
"eslint": ">=9.37.0",
5697
"eslint-mdx": ">=3.7.0",
5798
"eslint-plugin-mdx": ">=3.7.0",
5899
"eslint-plugin-react": ">=7.37.5",
100+
"oxfmt": ">=0.48.0",
59101
"remark-frontmatter": ">=5.0.0",
60102
"remark-lint-fenced-code-flag": ">=4.2.0",
61103
"remark-lint-final-newline": ">=3.0.1",
@@ -70,10 +112,5 @@
70112
"typescript": ">=5.9.3",
71113
"typescript-eslint": ">=8.31.1",
72114
"unified": ">=11"
73-
},
74-
"keywords": [
75-
"eslint",
76-
"config",
77-
"react"
78-
]
115+
}
79116
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
2+
// SPDX-License-Identifier: BSD-3-Clause-Clear
3+
4+
import recommended from "./recommended.js"
5+
6+
const mdxConfig = {
7+
configs: {
8+
recommended,
9+
},
10+
}
11+
12+
export default mdxConfig
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {readFile} from "node:fs/promises"
2+
import {dirname, isAbsolute, join, parse, resolve} from "node:path"
3+
import {pathToFileURL} from "node:url"
4+
import {format as formatCode, type FormatConfig} from "oxfmt"
5+
import {parseConfigFileTextToJson} from "typescript"
6+
7+
const CONFIG_FILENAMES = [".oxfmtrc.json", ".oxfmtrc.jsonc", "oxfmt.config.ts"]
8+
const configCache = new Map<string, Promise<FormatConfig>>()
9+
10+
export async function loadOxfmtConfig(
11+
filePath: string | undefined,
12+
): Promise<FormatConfig> {
13+
const basePath = getAbsolutePath(filePath)
14+
const startDir = parse(basePath).ext ? dirname(basePath) : basePath
15+
const cachedConfig = configCache.get(startDir)
16+
17+
if (cachedConfig) {
18+
return cachedConfig
19+
}
20+
21+
const configPromise = findOxfmtConfig(startDir).catch((error: unknown) => {
22+
configCache.delete(startDir)
23+
throw error
24+
})
25+
26+
configCache.set(startDir, configPromise)
27+
28+
return configPromise
29+
}
30+
31+
export async function formatOxfmt(
32+
filePath: string,
33+
value: string,
34+
config: FormatConfig,
35+
): Promise<null | string> {
36+
try {
37+
const result = await formatCode(filePath, value, config)
38+
39+
if (result.errors.length > 0) {
40+
return null
41+
}
42+
43+
return removeFinalEol(result.code)
44+
} catch {
45+
return null
46+
}
47+
}
48+
49+
export function getAbsolutePath(filePath: string | undefined): string {
50+
if (!filePath) {
51+
return process.cwd()
52+
}
53+
54+
return isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath)
55+
}
56+
57+
export function getErrorMessage(error: unknown): string {
58+
if (error instanceof Error) {
59+
return error.message
60+
}
61+
62+
return String(error)
63+
}
64+
65+
async function findOxfmtConfig(startDir: string): Promise<FormatConfig> {
66+
let currentDir = startDir
67+
68+
while (true) {
69+
for (const configFilename of CONFIG_FILENAMES) {
70+
const configPath = join(currentDir, configFilename)
71+
const config = await readOxfmtConfig(configPath, configFilename)
72+
73+
if (config) {
74+
return config
75+
}
76+
}
77+
78+
const parentDir = dirname(currentDir)
79+
80+
if (parentDir === currentDir) {
81+
return {}
82+
}
83+
84+
currentDir = parentDir
85+
}
86+
}
87+
88+
async function readOxfmtConfig(
89+
configPath: string,
90+
configFilename: string,
91+
): Promise<FormatConfig | null> {
92+
if (configFilename === "oxfmt.config.ts") {
93+
return readTypeScriptConfig(configPath)
94+
}
95+
96+
const source = await readOptionalFile(configPath)
97+
98+
if (source === null) {
99+
return null
100+
}
101+
102+
if (configFilename.endsWith(".jsonc")) {
103+
return validateConfig(configPath, readJsoncConfig(configPath, source))
104+
}
105+
106+
return validateConfig(configPath, JSON.parse(source))
107+
}
108+
109+
async function readTypeScriptConfig(
110+
configPath: string,
111+
): Promise<FormatConfig | null> {
112+
const source = await readOptionalFile(configPath)
113+
114+
if (source === null) {
115+
return null
116+
}
117+
118+
const url = pathToFileURL(configPath)
119+
url.searchParams.set("cache", Date.now().toString())
120+
121+
const module = (await import(url.href)) as {default?: unknown}
122+
123+
return validateConfig(configPath, module.default)
124+
}
125+
126+
function readJsoncConfig(configPath: string, source: string): unknown {
127+
const result = parseConfigFileTextToJson(configPath, source)
128+
129+
if (result.error) {
130+
throw new Error(String(result.error.messageText))
131+
}
132+
133+
return result.config
134+
}
135+
136+
function validateConfig(configPath: string, config: unknown): FormatConfig {
137+
if (!isPlainConfig(config)) {
138+
throw new Error(`${configPath} must define an object`)
139+
}
140+
141+
return config as FormatConfig
142+
}
143+
144+
async function readOptionalFile(path: string): Promise<null | string> {
145+
try {
146+
return await readFile(path, "utf8")
147+
} catch (error) {
148+
if (isMissingFileError(error)) {
149+
return null
150+
}
151+
152+
throw error
153+
}
154+
}
155+
156+
function removeFinalEol(value: string): string {
157+
return value.replace(/\r?\n$/u, "")
158+
}
159+
160+
function isPlainConfig(value: unknown): value is Record<string, unknown> {
161+
return typeof value === "object" && value !== null && !Array.isArray(value)
162+
}
163+
164+
function isMissingFileError(error: unknown): boolean {
165+
if (typeof error !== "object" || error === null || !("code" in error)) {
166+
return false
167+
}
168+
169+
const {code} = error
170+
171+
return code === "ENOENT" || code === "ENOTDIR"
172+
}

packages/configs/eslint-config-mdx/recommended.js renamed to packages/configs/eslint-config-mdx/src/recommended.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {defineConfig} from "eslint/config"
55
import * as mdx from "eslint-plugin-mdx"
66
import reactPlugin from "eslint-plugin-react"
77

8-
export default defineConfig({
8+
const recommended = defineConfig({
99
...mdx.flat,
1010
plugins: {
1111
...mdx.flat.plugins,
@@ -17,3 +17,5 @@ export default defineConfig({
1717
"no-unused-expressions": "off",
1818
},
1919
})
20+
21+
export default recommended

0 commit comments

Comments
 (0)