Skip to content

Commit

Permalink
feat: create preprocessed data (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
MengLinMaker authored Nov 9, 2024
1 parent dd746d8 commit 5e6a53d
Show file tree
Hide file tree
Showing 17 changed files with 488 additions and 699 deletions.
10 changes: 7 additions & 3 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ jobs:
with:
node-version: latest
cache: 'pnpm'

- run: pnpm install && pnpm build
- run: pnpm lint && pnpm test
# comfig recommended by pnpm error
- run: pnpm config set store-dir "/home/runner/setup-pnpm/node_modules/.bin/store/v3" --global

- run: pnpm install
- run: pnpm build
- run: pnpm lint
- run: pnpm test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Data
preprocessCompatData.json

# Test
coverage

Expand Down
10 changes: 2 additions & 8 deletions knip.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"ignore": ["src/*.ts", "**/tests/setup/file.ts"],
"ignore": ["**/tests/setup/file.ts"],
"ignoreBinaries": ["only-allow"],
"ignoreDependencies": [
"@changesets/changelog-github",
"@commitlint/cli",
"@commitlint/config-conventional",
"cz-git",
"@typescript-eslint/parser"
]
"ignoreDependencies": ["@changesets/changelog-github", "@typescript-eslint/parser"]
}
18 changes: 4 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,24 @@
"scripts": {
"preinstall": "npx only-allow pnpm",
"postinstall": "simple-git-hooks",
"commit": "czg",
"format": "biome check --write --verbose",
"lint": "tsc --noEmit --incremental",
"test": "vitest",
"build": "turbo build",
"clean": "turbo clean && rm -rf node_modules",
"clean": "rimraf packages/**/dist & rimraf .turbo packages/**/.turbo & rimraf node_modules packages/**/node_modules",
"version": "changeset version",
"release": "changeset publish",
"knip": "knip"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.15.4",
"@arethetypeswrong/cli": "0.16.4",
"@biomejs/biome": "^1.9.4",
"@changesets/changelog-github": "0.5.0",
"@changesets/cli": "^2.27.9",
"@commitlint/cli": "19.5.0",
"@commitlint/config-conventional": "19.4.1",
"@typescript-eslint/parser": "8.13.0",
"@typescript-eslint/rule-tester": "8.12.2",
"cz-git": "1.10.1",
"czg": "1.10.0",
"knip": "5.36.3",
"rimraf": "6.0.1",
"simple-git-hooks": "^2.11.1",
"ts-node": "10.9.2",
"tsup": "^8.3.5",
"turbo": "2.2.3",
"typescript": "5.6.3",
Expand All @@ -35,10 +30,5 @@
"pre-commit": "pnpm run format",
"pre-push": "pnpm run lint"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
},
"packageManager": "[email protected]"
}
31 changes: 31 additions & 0 deletions packages/data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# data - @eslint-plugin-runtime-compat/data

This is an internal package that preprocesses [`runtime-compat-data`](https://github.com/unjs/runtime-compat/tree/main/packages/runtime-compat-data):
- Classify API types: class, property access, globals...
- Identifies essential API information.
- Reduces multi-level JSON to 2 level JSON.
- Provides a filter for identifying unsupported APIs with given target runtimes.
- Expose minimal interface.

## Auto update

Building this package with `pnpm build` will:
1. Automatically update [`runtime-compat-data`](https://github.com/unjs/runtime-compat/tree/main/packages/runtime-compat-data)/.
2. Run preprocessing script to produce `preprocessCompatData.json`.
3. Finally build package.

## Runtime usage

Install in `packages.json` locally:
```Json
"@eslint-plugin-runtime-compat/data": "workspace:*"
```

Filter for targeted runtimes:
```TypeScript
import { type RuntimeName, filterPreprocessCompatData, preprocessCompatData } from '@eslint-plugin-runtime-compat/data'

const filterRuntimes: RuntimeName[] = ['node']

const runtimeCompatData = filterPreprocessCompatData(preprocessCompatData, filterRuntimes)
```
3 changes: 1 addition & 2 deletions packages/data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
"types": "./dist/index.d.ts",
"files": ["dist"],
"scripts": {
"commit": "czg",
"format": "biome check --write --verbose",
"lint": "tsc --noEmit --incremental",
"test": "vitest",
"build": "tsup src/index.ts --format cjs,esm --dts --clean --sourcemap",
"build": "pnpm install runtime-compat-data@latest && ts-node preprocess.ts && tsup src/index.ts --format cjs,esm --dts --clean --sourcemap",
"clean": "rm -rf node_modules"
},
"dependencies": {
Expand Down
113 changes: 113 additions & 0 deletions packages/data/preprocess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { writeFileSync } from 'node:fs'
import type { CompatStatement, Identifier, StatusBlock } from 'runtime-compat-data'
import rawCompatData from 'runtime-compat-data'
import type { PreprocessCompatData, PreprocessCompatStatement } from './src/types'

/**
* Compress raw compat data to single level flatmap
*/
const mapCompatData = new Map<string, PreprocessCompatStatement>()
{
const objectKeys = <T extends object>(object: T) => Object.keys(object) as (keyof T)[]

/**
* Simplifies compat data to only relevant info
* @param compatStatement The raw compat data API compat statement
* @returns Preprocess compat statement for runtime filtering before linting
*/
const extractPreprocessCompatStatement = (
compatStatement: CompatStatement,
): PreprocessCompatStatement => {
// Prefer MDN url
let url = compatStatement.mdn_url
if (url === undefined) {
if (Array.isArray(compatStatement.spec_url)) url = compatStatement.spec_url[0]
else url = compatStatement.spec_url
}
// Assume standard track if there is no API status
const defaultStatus = {
deprecated: false,
experimental: false,
standard_track: true,
} satisfies StatusBlock
return {
url: url ?? 'No url provided.',
status: compatStatement.status ?? defaultStatus,
support: compatStatement.support,
}
}

/**
* DFS parse raw compat data
* @param compatData Raw compat data and inner data
* @param parentKeys Chain of keys with '__compat' as parameter
*/
const parseRawCompatData = (compatData: Identifier, parentKeys: string[] = []) => {
const keys = objectKeys(compatData) as string[]
for (const key of keys) {
const subData = compatData[key]
if (key === '__compat') {
const finalCompatStatement = extractPreprocessCompatStatement(subData as never)
mapCompatData.set(JSON.stringify(parentKeys), finalCompatStatement)
} else {
// Only chain keys if "__compat" exists
const nodeHasCompatData = !keys.includes('__compat')
const filteredParentKeys = nodeHasCompatData ? [key] : [...parentKeys, key]
if (subData) parseRawCompatData(subData, filteredParentKeys)
}
}
}
parseRawCompatData(rawCompatData.api as never)
}

/**
* Sort mapped compat data into different AST detection scenarios
*/
const preprocessCompatData: PreprocessCompatData = {
class: {},
classProperty: {},
eventListener: {},
global: {},
globalClassProperty: {},
misc: {},
}
{
const isPascalCase = (s: string | undefined) => s?.match(/^[A-Z]+.*/)
for (const [jsonKeys, finalCompatStatement] of mapCompatData.entries()) {
const keys = JSON.parse(jsonKeys) as string[]
if (keys.length === 1) {
if (isPascalCase(keys[0])) {
// PascalCase, hence a class
preprocessCompatData.class[jsonKeys] = finalCompatStatement
} else {
// camelCase, hence a variable or function
preprocessCompatData.global[jsonKeys] = finalCompatStatement
}
} else if (keys.length === 2) {
if (keys[0] === keys[1])
// Duplicate keys are class constructors
preprocessCompatData.class[JSON.stringify([keys[0]])] = finalCompatStatement
else if (keys[1]?.match('_static')) {
// Static methods have '_static'
const newKeys = JSON.stringify([keys[0], keys[1]?.replace('_static', '')])
if (isPascalCase(keys[0]))
preprocessCompatData.classProperty[newKeys] = finalCompatStatement
else preprocessCompatData.globalClassProperty[newKeys] = finalCompatStatement
} else if (keys[1]?.match('_event')) {
// Events have '_event'
const newKeys = JSON.stringify([keys[0], keys[1]?.replace('_event', '')])
preprocessCompatData.eventListener[newKeys] = finalCompatStatement
} else if (!keys[1]?.match('_'))
// Normal class property
preprocessCompatData.classProperty[jsonKeys] = finalCompatStatement
else preprocessCompatData.misc[jsonKeys] = finalCompatStatement
} else {
// Not sure how to analyse
preprocessCompatData.misc[JSON.stringify([keys[0]])] = finalCompatStatement
}
}
}
writeFileSync(
'./src/preprocessCompatData.json',
`${JSON.stringify(preprocessCompatData, null, 2)}\n`,
)
3 changes: 2 additions & 1 deletion packages/data/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { RuntimeName } from 'runtime-compat-data'
import data from 'runtime-compat-data'
import { filterSupportCompatData } from './filterSupportCompatData.js'
import { mapCompatData } from './mapCompatData.js'
import type { ParsedCompatData, RuleConfig } from './types.js'

export type { RuleConfig, ParsedCompatData, RuntimeName }
export { filterSupportCompatData, mapCompatData }
export { filterSupportCompatData, mapCompatData, data }
80 changes: 80 additions & 0 deletions packages/data/src/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import _preprocessCompatData from './preprocessCompatData.json'

import type { RuntimeName } from 'runtime-compat-data'
import { objectKeys } from './objectKeys'
import type {
PreprocessCompatData,
PreprocessCompatStatement,
RuntimeCompatData,
RuntimeCompatStatement,
} from './types.js'

const preprocessCompatData: PreprocessCompatData = _preprocessCompatData
export { preprocessCompatData }

/**
* Extract unsupported runtimes based of filter.
* @param preprocessCompatStatement
* @param filterRuntimes - Runtimes to filter for lack of support detection.
* @returns Array of unsupported runtimes.
*/
const getUnsupportedRuntimes = (
preprocessCompatStatement: PreprocessCompatStatement,
filterRuntimes: RuntimeName[],
) => {
const unsupportedRuntimes: RuntimeName[] = []

for (const filterRuntime of filterRuntimes) {
const support = preprocessCompatStatement.support[filterRuntime]
if (support === undefined) {
// Runtime not found, therefore unsupported.
unsupportedRuntimes.push(filterRuntime)
} else if (Array.isArray(support)) {
// Array format not supported by runtime-compat-data, therefore unsupported.
unsupportedRuntimes.push(filterRuntime)
} else if (support.version_added === false) {
// Only boolean is supported in runtime-compat-data.
unsupportedRuntimes.push(filterRuntime)
}
}
return unsupportedRuntimes
}

/**
* Clean flat compat data object, retaining only unsupported runtimes.
* @param flatCompatData - Flat compat data object.
* @param filterRuntimes - Runtimes to filter for lack of support detection.
* @returns Parsed unsupported runtime data.
*/
export const filterPreprocessCompatData = (
preprocessCompatData: PreprocessCompatData,
filterRuntimes: RuntimeName[],
) => {
const parsedCompatData: RuntimeCompatData = {
class: new Map<string, RuntimeCompatStatement>(),
classProperty: new Map<string, RuntimeCompatStatement>(),
eventListener: new Map<string, RuntimeCompatStatement>(),
global: new Map<string, RuntimeCompatStatement>(),
globalClassProperty: new Map<string, RuntimeCompatStatement>(),
misc: new Map<string, RuntimeCompatStatement>(),
}
for (const context of objectKeys(preprocessCompatData)) {
for (const jsonKeys of objectKeys(preprocessCompatData[context])) {
const preprocessCompatStatement = preprocessCompatData[context][jsonKeys]
if (preprocessCompatStatement) {
const unsupportedRuntimes = getUnsupportedRuntimes(
preprocessCompatStatement,
filterRuntimes,
)
if (unsupportedRuntimes.length > 0) {
parsedCompatData[context].set(jsonKeys, {
url: preprocessCompatStatement.url,
status: preprocessCompatStatement.status,
unsupported: unsupportedRuntimes,
})
}
}
}
}
return parsedCompatData
}
19 changes: 19 additions & 0 deletions packages/data/src/tests/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { RuntimeName } from 'runtime-compat-data'
import { describe, expect, it } from 'vitest'
import { objectKeys } from '../objectKeys'
import { filterPreprocessCompatData, preprocessCompatData } from '../runtime'

describe('filterPreprocessCompatData', () => {
const filterRuntimes: RuntimeName[] = ['node']

it('should successfully filter for ', () => {
const runtimeCompatData = filterPreprocessCompatData(preprocessCompatData, filterRuntimes)
for (const context of objectKeys(runtimeCompatData)) {
for (const [jsonKeys, runtimeCompatStatement] of runtimeCompatData[context].entries()) {
const keys = JSON.parse(jsonKeys) as string[]
expect(keys.length).toBeGreaterThan(0)
expect(runtimeCompatStatement.unsupported).toStrictEqual(filterRuntimes)
}
}
})
})
Loading

0 comments on commit 5e6a53d

Please sign in to comment.