Skip to content

Commit 5e6a53d

Browse files
authored
feat: create preprocessed data (#55)
1 parent dd746d8 commit 5e6a53d

File tree

17 files changed

+488
-699
lines changed

17 files changed

+488
-699
lines changed

.github/workflows/CI.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ jobs:
1616
with:
1717
node-version: latest
1818
cache: 'pnpm'
19-
20-
- run: pnpm install && pnpm build
21-
- run: pnpm lint && pnpm test
19+
# comfig recommended by pnpm error
20+
- run: pnpm config set store-dir "/home/runner/setup-pnpm/node_modules/.bin/store/v3" --global
21+
22+
- run: pnpm install
23+
- run: pnpm build
24+
- run: pnpm lint
25+
- run: pnpm test

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Data
2+
preprocessCompatData.json
3+
14
# Test
25
coverage
36

knip.json

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
{
22
"$schema": "https://unpkg.com/knip@5/schema.json",
3-
"ignore": ["src/*.ts", "**/tests/setup/file.ts"],
3+
"ignore": ["**/tests/setup/file.ts"],
44
"ignoreBinaries": ["only-allow"],
5-
"ignoreDependencies": [
6-
"@changesets/changelog-github",
7-
"@commitlint/cli",
8-
"@commitlint/config-conventional",
9-
"cz-git",
10-
"@typescript-eslint/parser"
11-
]
5+
"ignoreDependencies": ["@changesets/changelog-github", "@typescript-eslint/parser"]
126
}

package.json

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,24 @@
33
"scripts": {
44
"preinstall": "npx only-allow pnpm",
55
"postinstall": "simple-git-hooks",
6-
"commit": "czg",
76
"format": "biome check --write --verbose",
87
"lint": "tsc --noEmit --incremental",
98
"test": "vitest",
109
"build": "turbo build",
11-
"clean": "turbo clean && rm -rf node_modules",
10+
"clean": "rimraf packages/**/dist & rimraf .turbo packages/**/.turbo & rimraf node_modules packages/**/node_modules",
1211
"version": "changeset version",
1312
"release": "changeset publish",
1413
"knip": "knip"
1514
},
1615
"devDependencies": {
17-
"@arethetypeswrong/cli": "^0.15.4",
16+
"@arethetypeswrong/cli": "0.16.4",
1817
"@biomejs/biome": "^1.9.4",
1918
"@changesets/changelog-github": "0.5.0",
2019
"@changesets/cli": "^2.27.9",
21-
"@commitlint/cli": "19.5.0",
22-
"@commitlint/config-conventional": "19.4.1",
23-
"@typescript-eslint/parser": "8.13.0",
24-
"@typescript-eslint/rule-tester": "8.12.2",
25-
"cz-git": "1.10.1",
26-
"czg": "1.10.0",
2720
"knip": "5.36.3",
21+
"rimraf": "6.0.1",
2822
"simple-git-hooks": "^2.11.1",
23+
"ts-node": "10.9.2",
2924
"tsup": "^8.3.5",
3025
"turbo": "2.2.3",
3126
"typescript": "5.6.3",
@@ -35,10 +30,5 @@
3530
"pre-commit": "pnpm run format",
3631
"pre-push": "pnpm run lint"
3732
},
38-
"config": {
39-
"commitizen": {
40-
"path": "node_modules/cz-git"
41-
}
42-
},
4333
"packageManager": "[email protected]"
4434
}

packages/data/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# data - @eslint-plugin-runtime-compat/data
2+
3+
This is an internal package that preprocesses [`runtime-compat-data`](https://github.com/unjs/runtime-compat/tree/main/packages/runtime-compat-data):
4+
- Classify API types: class, property access, globals...
5+
- Identifies essential API information.
6+
- Reduces multi-level JSON to 2 level JSON.
7+
- Provides a filter for identifying unsupported APIs with given target runtimes.
8+
- Expose minimal interface.
9+
10+
## Auto update
11+
12+
Building this package with `pnpm build` will:
13+
1. Automatically update [`runtime-compat-data`](https://github.com/unjs/runtime-compat/tree/main/packages/runtime-compat-data)/.
14+
2. Run preprocessing script to produce `preprocessCompatData.json`.
15+
3. Finally build package.
16+
17+
## Runtime usage
18+
19+
Install in `packages.json` locally:
20+
```Json
21+
"@eslint-plugin-runtime-compat/data": "workspace:*"
22+
```
23+
24+
Filter for targeted runtimes:
25+
```TypeScript
26+
import { type RuntimeName, filterPreprocessCompatData, preprocessCompatData } from '@eslint-plugin-runtime-compat/data'
27+
28+
const filterRuntimes: RuntimeName[] = ['node']
29+
30+
const runtimeCompatData = filterPreprocessCompatData(preprocessCompatData, filterRuntimes)
31+
```

packages/data/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
"types": "./dist/index.d.ts",
66
"files": ["dist"],
77
"scripts": {
8-
"commit": "czg",
98
"format": "biome check --write --verbose",
109
"lint": "tsc --noEmit --incremental",
1110
"test": "vitest",
12-
"build": "tsup src/index.ts --format cjs,esm --dts --clean --sourcemap",
11+
"build": "pnpm install runtime-compat-data@latest && ts-node preprocess.ts && tsup src/index.ts --format cjs,esm --dts --clean --sourcemap",
1312
"clean": "rm -rf node_modules"
1413
},
1514
"dependencies": {

packages/data/preprocess.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { writeFileSync } from 'node:fs'
2+
import type { CompatStatement, Identifier, StatusBlock } from 'runtime-compat-data'
3+
import rawCompatData from 'runtime-compat-data'
4+
import type { PreprocessCompatData, PreprocessCompatStatement } from './src/types'
5+
6+
/**
7+
* Compress raw compat data to single level flatmap
8+
*/
9+
const mapCompatData = new Map<string, PreprocessCompatStatement>()
10+
{
11+
const objectKeys = <T extends object>(object: T) => Object.keys(object) as (keyof T)[]
12+
13+
/**
14+
* Simplifies compat data to only relevant info
15+
* @param compatStatement The raw compat data API compat statement
16+
* @returns Preprocess compat statement for runtime filtering before linting
17+
*/
18+
const extractPreprocessCompatStatement = (
19+
compatStatement: CompatStatement,
20+
): PreprocessCompatStatement => {
21+
// Prefer MDN url
22+
let url = compatStatement.mdn_url
23+
if (url === undefined) {
24+
if (Array.isArray(compatStatement.spec_url)) url = compatStatement.spec_url[0]
25+
else url = compatStatement.spec_url
26+
}
27+
// Assume standard track if there is no API status
28+
const defaultStatus = {
29+
deprecated: false,
30+
experimental: false,
31+
standard_track: true,
32+
} satisfies StatusBlock
33+
return {
34+
url: url ?? 'No url provided.',
35+
status: compatStatement.status ?? defaultStatus,
36+
support: compatStatement.support,
37+
}
38+
}
39+
40+
/**
41+
* DFS parse raw compat data
42+
* @param compatData Raw compat data and inner data
43+
* @param parentKeys Chain of keys with '__compat' as parameter
44+
*/
45+
const parseRawCompatData = (compatData: Identifier, parentKeys: string[] = []) => {
46+
const keys = objectKeys(compatData) as string[]
47+
for (const key of keys) {
48+
const subData = compatData[key]
49+
if (key === '__compat') {
50+
const finalCompatStatement = extractPreprocessCompatStatement(subData as never)
51+
mapCompatData.set(JSON.stringify(parentKeys), finalCompatStatement)
52+
} else {
53+
// Only chain keys if "__compat" exists
54+
const nodeHasCompatData = !keys.includes('__compat')
55+
const filteredParentKeys = nodeHasCompatData ? [key] : [...parentKeys, key]
56+
if (subData) parseRawCompatData(subData, filteredParentKeys)
57+
}
58+
}
59+
}
60+
parseRawCompatData(rawCompatData.api as never)
61+
}
62+
63+
/**
64+
* Sort mapped compat data into different AST detection scenarios
65+
*/
66+
const preprocessCompatData: PreprocessCompatData = {
67+
class: {},
68+
classProperty: {},
69+
eventListener: {},
70+
global: {},
71+
globalClassProperty: {},
72+
misc: {},
73+
}
74+
{
75+
const isPascalCase = (s: string | undefined) => s?.match(/^[A-Z]+.*/)
76+
for (const [jsonKeys, finalCompatStatement] of mapCompatData.entries()) {
77+
const keys = JSON.parse(jsonKeys) as string[]
78+
if (keys.length === 1) {
79+
if (isPascalCase(keys[0])) {
80+
// PascalCase, hence a class
81+
preprocessCompatData.class[jsonKeys] = finalCompatStatement
82+
} else {
83+
// camelCase, hence a variable or function
84+
preprocessCompatData.global[jsonKeys] = finalCompatStatement
85+
}
86+
} else if (keys.length === 2) {
87+
if (keys[0] === keys[1])
88+
// Duplicate keys are class constructors
89+
preprocessCompatData.class[JSON.stringify([keys[0]])] = finalCompatStatement
90+
else if (keys[1]?.match('_static')) {
91+
// Static methods have '_static'
92+
const newKeys = JSON.stringify([keys[0], keys[1]?.replace('_static', '')])
93+
if (isPascalCase(keys[0]))
94+
preprocessCompatData.classProperty[newKeys] = finalCompatStatement
95+
else preprocessCompatData.globalClassProperty[newKeys] = finalCompatStatement
96+
} else if (keys[1]?.match('_event')) {
97+
// Events have '_event'
98+
const newKeys = JSON.stringify([keys[0], keys[1]?.replace('_event', '')])
99+
preprocessCompatData.eventListener[newKeys] = finalCompatStatement
100+
} else if (!keys[1]?.match('_'))
101+
// Normal class property
102+
preprocessCompatData.classProperty[jsonKeys] = finalCompatStatement
103+
else preprocessCompatData.misc[jsonKeys] = finalCompatStatement
104+
} else {
105+
// Not sure how to analyse
106+
preprocessCompatData.misc[JSON.stringify([keys[0]])] = finalCompatStatement
107+
}
108+
}
109+
}
110+
writeFileSync(
111+
'./src/preprocessCompatData.json',
112+
`${JSON.stringify(preprocessCompatData, null, 2)}\n`,
113+
)

packages/data/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { RuntimeName } from 'runtime-compat-data'
2+
import data from 'runtime-compat-data'
23
import { filterSupportCompatData } from './filterSupportCompatData.js'
34
import { mapCompatData } from './mapCompatData.js'
45
import type { ParsedCompatData, RuleConfig } from './types.js'
56

67
export type { RuleConfig, ParsedCompatData, RuntimeName }
7-
export { filterSupportCompatData, mapCompatData }
8+
export { filterSupportCompatData, mapCompatData, data }

packages/data/src/runtime.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import _preprocessCompatData from './preprocessCompatData.json'
2+
3+
import type { RuntimeName } from 'runtime-compat-data'
4+
import { objectKeys } from './objectKeys'
5+
import type {
6+
PreprocessCompatData,
7+
PreprocessCompatStatement,
8+
RuntimeCompatData,
9+
RuntimeCompatStatement,
10+
} from './types.js'
11+
12+
const preprocessCompatData: PreprocessCompatData = _preprocessCompatData
13+
export { preprocessCompatData }
14+
15+
/**
16+
* Extract unsupported runtimes based of filter.
17+
* @param preprocessCompatStatement
18+
* @param filterRuntimes - Runtimes to filter for lack of support detection.
19+
* @returns Array of unsupported runtimes.
20+
*/
21+
const getUnsupportedRuntimes = (
22+
preprocessCompatStatement: PreprocessCompatStatement,
23+
filterRuntimes: RuntimeName[],
24+
) => {
25+
const unsupportedRuntimes: RuntimeName[] = []
26+
27+
for (const filterRuntime of filterRuntimes) {
28+
const support = preprocessCompatStatement.support[filterRuntime]
29+
if (support === undefined) {
30+
// Runtime not found, therefore unsupported.
31+
unsupportedRuntimes.push(filterRuntime)
32+
} else if (Array.isArray(support)) {
33+
// Array format not supported by runtime-compat-data, therefore unsupported.
34+
unsupportedRuntimes.push(filterRuntime)
35+
} else if (support.version_added === false) {
36+
// Only boolean is supported in runtime-compat-data.
37+
unsupportedRuntimes.push(filterRuntime)
38+
}
39+
}
40+
return unsupportedRuntimes
41+
}
42+
43+
/**
44+
* Clean flat compat data object, retaining only unsupported runtimes.
45+
* @param flatCompatData - Flat compat data object.
46+
* @param filterRuntimes - Runtimes to filter for lack of support detection.
47+
* @returns Parsed unsupported runtime data.
48+
*/
49+
export const filterPreprocessCompatData = (
50+
preprocessCompatData: PreprocessCompatData,
51+
filterRuntimes: RuntimeName[],
52+
) => {
53+
const parsedCompatData: RuntimeCompatData = {
54+
class: new Map<string, RuntimeCompatStatement>(),
55+
classProperty: new Map<string, RuntimeCompatStatement>(),
56+
eventListener: new Map<string, RuntimeCompatStatement>(),
57+
global: new Map<string, RuntimeCompatStatement>(),
58+
globalClassProperty: new Map<string, RuntimeCompatStatement>(),
59+
misc: new Map<string, RuntimeCompatStatement>(),
60+
}
61+
for (const context of objectKeys(preprocessCompatData)) {
62+
for (const jsonKeys of objectKeys(preprocessCompatData[context])) {
63+
const preprocessCompatStatement = preprocessCompatData[context][jsonKeys]
64+
if (preprocessCompatStatement) {
65+
const unsupportedRuntimes = getUnsupportedRuntimes(
66+
preprocessCompatStatement,
67+
filterRuntimes,
68+
)
69+
if (unsupportedRuntimes.length > 0) {
70+
parsedCompatData[context].set(jsonKeys, {
71+
url: preprocessCompatStatement.url,
72+
status: preprocessCompatStatement.status,
73+
unsupported: unsupportedRuntimes,
74+
})
75+
}
76+
}
77+
}
78+
}
79+
return parsedCompatData
80+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { RuntimeName } from 'runtime-compat-data'
2+
import { describe, expect, it } from 'vitest'
3+
import { objectKeys } from '../objectKeys'
4+
import { filterPreprocessCompatData, preprocessCompatData } from '../runtime'
5+
6+
describe('filterPreprocessCompatData', () => {
7+
const filterRuntimes: RuntimeName[] = ['node']
8+
9+
it('should successfully filter for ', () => {
10+
const runtimeCompatData = filterPreprocessCompatData(preprocessCompatData, filterRuntimes)
11+
for (const context of objectKeys(runtimeCompatData)) {
12+
for (const [jsonKeys, runtimeCompatStatement] of runtimeCompatData[context].entries()) {
13+
const keys = JSON.parse(jsonKeys) as string[]
14+
expect(keys.length).toBeGreaterThan(0)
15+
expect(runtimeCompatStatement.unsupported).toStrictEqual(filterRuntimes)
16+
}
17+
}
18+
})
19+
})

0 commit comments

Comments
 (0)