Update and refactor
This commit is contained in:
356
scripts/build.ts
Executable file
356
scripts/build.ts
Executable file
@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env tsx
|
||||
import assert from 'node:assert';
|
||||
import { readFileSync, promises as fs } from 'node:fs';
|
||||
import { resolve, extname, relative } from 'node:path';
|
||||
import { isBuiltin } from 'node:module';
|
||||
import esbuild from 'esbuild';
|
||||
import type { Loader, Plugin } from 'esbuild';
|
||||
import * as babel from '@babel/core';
|
||||
import { memoize } from 'lodash';
|
||||
import { gray, green } from 'picocolors';
|
||||
import type { types as t, types } from '@babel/core';
|
||||
import { dependencies } from '../dist/package.json';
|
||||
import { createMacro, type MacroHandler } from 'babel-plugin-macros';
|
||||
import * as polyfill from '../src/polyfill';
|
||||
import { buildLocalRules } from '../src/build-local-rules';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const polyfills = Object.keys(polyfill);
|
||||
|
||||
const ENV = (process.env.NODE_ENV ??= 'production');
|
||||
const PROD = ENV === 'production';
|
||||
|
||||
class HandlerMap {
|
||||
map = new Map<string, MacroHandler>();
|
||||
|
||||
set(names: string | string[], handler: MacroHandler) {
|
||||
names = Array.isArray(names) ? names : [names];
|
||||
const macro = createMacro(handler);
|
||||
for (const name of names) {
|
||||
this.map.set(name, macro);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
get keys() {
|
||||
return Array.from(this.map.keys());
|
||||
}
|
||||
|
||||
resolvePath = (module: string) => module;
|
||||
require = (module: string) => this.map.get(module);
|
||||
isMacrosName = (module: string) => this.map.has(module);
|
||||
}
|
||||
|
||||
const map = new HandlerMap()
|
||||
.set(
|
||||
'object.assign',
|
||||
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('assign'))),
|
||||
)
|
||||
.set(
|
||||
['object-values', 'object.values'],
|
||||
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('values'))),
|
||||
)
|
||||
.set(
|
||||
'object.fromentries',
|
||||
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('fromEntries'))),
|
||||
)
|
||||
.set(
|
||||
'object.entries',
|
||||
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('entries'))),
|
||||
)
|
||||
.set(
|
||||
'hasown',
|
||||
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
|
||||
)
|
||||
.set(
|
||||
'has',
|
||||
replace(t => t.memberExpression(t.identifier('Object'), t.identifier('hasOwn'))),
|
||||
)
|
||||
.set(
|
||||
'array-includes',
|
||||
proto(t => t.identifier('includes')),
|
||||
)
|
||||
.set(
|
||||
'array.prototype.flatmap',
|
||||
proto(t => t.identifier('flatMap')),
|
||||
)
|
||||
.set(
|
||||
'array.prototype.flat',
|
||||
proto(t => t.identifier('flat')),
|
||||
)
|
||||
.set(
|
||||
'array.prototype.findlastindex',
|
||||
proto(t => t.identifier('findLastIndex')),
|
||||
)
|
||||
.set(
|
||||
'array.prototype.tosorted',
|
||||
proto(t => t.identifier('toSorted')),
|
||||
)
|
||||
.set(
|
||||
'array.prototype.toreversed',
|
||||
proto(t => t.identifier('toReversed')),
|
||||
)
|
||||
.set(
|
||||
'array.prototype.findlast',
|
||||
proto(t => t.identifier('findLast')),
|
||||
)
|
||||
.set(
|
||||
'string.prototype.matchall',
|
||||
proto(t => t.identifier('matchAll')),
|
||||
)
|
||||
.set(
|
||||
'string.prototype.includes',
|
||||
proto(t => t.identifier('includes')),
|
||||
)
|
||||
.set(
|
||||
'object.groupby',
|
||||
replace(t =>
|
||||
t.memberExpression(
|
||||
t.callExpression(t.identifier('require'), [t.stringLiteral('lodash')]),
|
||||
t.identifier('groupBy'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// es-iterator-helpers/Iterator.prototype.*
|
||||
const polyfillPath = resolve(__dirname, '../src/polyfill.ts');
|
||||
const requirePolyfill = (t: typeof types, name: string) =>
|
||||
t.memberExpression(
|
||||
t.callExpression(t.identifier('require'), [t.stringLiteral(polyfillPath)]),
|
||||
t.identifier(name),
|
||||
);
|
||||
map.set(
|
||||
`es-iterator-helpers/Iterator.from`,
|
||||
replace(t => requirePolyfill(t, 'from')),
|
||||
);
|
||||
for (const name of polyfills) {
|
||||
map.set(
|
||||
`es-iterator-helpers/Iterator.prototype.${name}`,
|
||||
replace(t => requirePolyfill(t, name)),
|
||||
);
|
||||
}
|
||||
|
||||
map.set(
|
||||
'safe-regex-test',
|
||||
replace(t => requirePolyfill(t, 'safeRegexTest')),
|
||||
);
|
||||
|
||||
function replace(getReplacement: (types: typeof t) => t.Expression): MacroHandler {
|
||||
return ({ references, babel: { types: t } }) => {
|
||||
references.default.forEach(referencePath => {
|
||||
referencePath.replaceWith(getReplacement(t));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function proto(getProperty: (types: typeof t) => t.Expression): MacroHandler {
|
||||
return ({ references, babel: { types: t } }) => {
|
||||
references.default.forEach(referencePath => {
|
||||
const { parent, parentPath } = referencePath;
|
||||
assert(t.isCallExpression(parent));
|
||||
const [callee, ...rest] = parent.arguments;
|
||||
parentPath!.replaceWith(
|
||||
t.callExpression(
|
||||
t.memberExpression(callee as t.Expression, getProperty(t)),
|
||||
rest,
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const babelPlugin: Plugin = {
|
||||
name: 'babel',
|
||||
setup(build) {
|
||||
const { keys, ...macroOptions } = map;
|
||||
|
||||
build.onLoad({ filter: /\.[jt]sx?$/ }, args => {
|
||||
const { path } = args;
|
||||
if (path.includes('node_modules/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let source = readFileSync(path, 'utf-8')
|
||||
.replaceAll("require('object.hasown/polyfill')()", 'Object.hasOwn')
|
||||
.replaceAll("require('object.fromentries/polyfill')()", 'Object.fromEntries')
|
||||
.replaceAll(
|
||||
"Object.keys(require('prop-types'))",
|
||||
JSON.stringify(Object.keys(require('prop-types'))),
|
||||
);
|
||||
|
||||
if (
|
||||
path.includes('packages/eslint-plugin-import/src/rules/') ||
|
||||
path.includes('packages/eslint-plugin-import/config/')
|
||||
) {
|
||||
source = source.replace('\nmodule.exports = {', '\nexport default {');
|
||||
}
|
||||
|
||||
const isFlow = source.includes('@flow');
|
||||
const loader = extname(path).slice(1) as Loader;
|
||||
|
||||
if (!isFlow && !keys.some(key => source.includes(key))) {
|
||||
return { contents: source, loader };
|
||||
}
|
||||
|
||||
const res = babel.transformSync(source, {
|
||||
filename: path,
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
parserOpts: {
|
||||
plugins: [isFlow ? 'flow' : 'typescript'],
|
||||
},
|
||||
plugins: [
|
||||
isFlow && '@babel/plugin-transform-flow-strip-types',
|
||||
['babel-plugin-macros', macroOptions],
|
||||
].filter(Boolean),
|
||||
})!;
|
||||
|
||||
return {
|
||||
contents: res.code!,
|
||||
loader,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Array<T> {
|
||||
filter(
|
||||
predicate: BooleanConstructor,
|
||||
): Exclude<T, null | undefined | false | '' | 0>[];
|
||||
}
|
||||
}
|
||||
|
||||
const log = memoize(console.log);
|
||||
|
||||
const plugins: Plugin[] = [
|
||||
babelPlugin,
|
||||
{
|
||||
name: 'alias',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^jsx-ast-utils$/ }, () => ({
|
||||
path: resolve('./packages/jsx-ast-utils/src/index.js'),
|
||||
}));
|
||||
build.onResolve({ filter: /^jsx-ast-utils\/.+$/ }, ({ path }) => ({
|
||||
path:
|
||||
resolve('./packages/jsx-ast-utils/', path.slice('jsx-ast-utils/'.length)) +
|
||||
'.js',
|
||||
}));
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
plugins.push({
|
||||
name: 'deps-check',
|
||||
setup(build) {
|
||||
const declared = new Set(Object.keys(dependencies));
|
||||
|
||||
build.onResolve({ filter: /^.*$/ }, ({ path, importer }) => {
|
||||
if (
|
||||
!path.startsWith('./') &&
|
||||
!path.startsWith('../') &&
|
||||
!isBuiltin(path) &&
|
||||
path !== 'eslint' &&
|
||||
!path.startsWith('eslint/') &&
|
||||
!path.startsWith('eslint-module-utils/') &&
|
||||
!declared.has(path)
|
||||
) {
|
||||
log(green(path), gray('from'), './' + relative(process.cwd(), importer));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function bundle(
|
||||
entry: string,
|
||||
outfile = entry
|
||||
.replace('./packages/', './dist/')
|
||||
.replace('src/', '')
|
||||
.replace('.ts', '.js'),
|
||||
) {
|
||||
await esbuild.build({
|
||||
entryPoints: [entry],
|
||||
outfile,
|
||||
bundle: true,
|
||||
minify: PROD,
|
||||
platform: 'node',
|
||||
packages: 'external',
|
||||
sourcemap: 'linked',
|
||||
plugins,
|
||||
define: {},
|
||||
alias: {
|
||||
// esm modules
|
||||
},
|
||||
external: ['find-cache-dir'],
|
||||
banner: {
|
||||
js: '/* eslint-disable */',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function editPackageJson() {
|
||||
const [state, setState] = await useText('./dist/package.json');
|
||||
const distPackageJson = JSON.parse(state);
|
||||
|
||||
const overrideList = await fs.readdir('dist/overrides');
|
||||
const npmOverrides = Object.fromEntries(
|
||||
overrideList.map(name => [name, `file:./overrides/${name}`]),
|
||||
);
|
||||
Object.assign(distPackageJson, {
|
||||
overrides: npmOverrides,
|
||||
resolutions: Object.fromEntries(
|
||||
overrideList.map(name => [`**/${name}`, `file:./overrides/${name}`]),
|
||||
),
|
||||
pnpm: { overrides: npmOverrides },
|
||||
});
|
||||
|
||||
await setState(JSON.stringify(distPackageJson, null, 2) + '\n');
|
||||
}
|
||||
|
||||
async function useText(path: string) {
|
||||
const state = await fs.readFile(path, 'utf-8');
|
||||
const setState = (text: string) => fs.writeFile(path, text);
|
||||
return [state, setState] as const;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Building local rules...');
|
||||
await buildLocalRules();
|
||||
|
||||
console.log('Building type definitions...');
|
||||
execSync(
|
||||
[
|
||||
'npx',
|
||||
'dts-bundle-generator',
|
||||
'"./src/index.ts"',
|
||||
'-o',
|
||||
'"./dist/index.d.ts"',
|
||||
'--project',
|
||||
'"./tsconfig.build.json"',
|
||||
'--no-check',
|
||||
].join(' '),
|
||||
);
|
||||
|
||||
console.log('Building packages...');
|
||||
await Promise.all([
|
||||
bundle('./packages/eslint-plugin-react/index.js'),
|
||||
bundle('./packages/eslint-plugin-import/src/index.js'),
|
||||
bundle('./packages/eslint-plugin-jsx-a11y/src/index.js'),
|
||||
bundle('./packages/eslint-plugin-react-hooks/index.ts'),
|
||||
bundle('./packages/eslint-plugin-n/lib/index.js', './dist/eslint-plugin-n/index.js'),
|
||||
bundle('./packages/eslint-import-resolver-typescript/src/index.ts'),
|
||||
bundle('./src/rules/index.ts', './dist/eslint-plugin-rules/index.js'),
|
||||
bundle('./src/local/index.ts', './dist/eslint-plugin-local/index.js'),
|
||||
bundle('./src/index.ts', './dist/index.js'),
|
||||
editPackageJson(),
|
||||
]);
|
||||
|
||||
console.log('Removing redirect...');
|
||||
const [distIndex, setDistIndex] = await useText('./dist/index.js');
|
||||
await setDistIndex(distIndex.replace(/import.*redirect.*;/g, ''));
|
||||
}
|
||||
|
||||
void main();
|
69
scripts/check-imports.ts
Executable file
69
scripts/check-imports.ts
Executable file
@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bun
|
||||
import glob from 'fast-glob';
|
||||
import fs from 'fs';
|
||||
import { builtinModules } from 'module';
|
||||
import { uniq } from 'lodash';
|
||||
import { dependencies, peerDependencies, overrides } from '../dist/package.json';
|
||||
|
||||
function checkImports() {
|
||||
const deps = Object.keys({ ...dependencies, ...peerDependencies, ...overrides }).concat(
|
||||
'eslint',
|
||||
);
|
||||
const builtIn = new Set(builtinModules.flatMap(module => [module, `node:${module}`]));
|
||||
|
||||
function findRequires(text: string) {
|
||||
const list = Array.from(text.matchAll(/require\(["']([^"']+)["']\)/g))
|
||||
.map(m => m[1])
|
||||
.filter(
|
||||
module =>
|
||||
!(
|
||||
builtIn.has(module) ||
|
||||
module.startsWith('eslint/') ||
|
||||
module.startsWith('typescript/')
|
||||
),
|
||||
);
|
||||
return uniq(list);
|
||||
}
|
||||
|
||||
const moduleMap = glob
|
||||
.sync(['dist/**/*.js', '!dist/node_modules/**'])
|
||||
.map(path => ({ key: path, value: findRequires(fs.readFileSync(path, 'utf8')) }));
|
||||
|
||||
const files = Object.fromEntries(
|
||||
moduleMap
|
||||
.map(({ key, value }) => ({
|
||||
key,
|
||||
value: value.filter(
|
||||
module =>
|
||||
!(deps.includes(module) || deps.some(dep => module.startsWith(`${dep}/`))),
|
||||
),
|
||||
}))
|
||||
.filter(({ value }) => value.length > 0)
|
||||
.map(({ key, value }) => [key, value]),
|
||||
);
|
||||
const uselessDeps = Object.keys(dependencies).filter(
|
||||
dep => !moduleMap.some(({ value }) => value.includes(dep)),
|
||||
);
|
||||
|
||||
return {
|
||||
missingImports: files,
|
||||
unusedDependencies: uselessDeps,
|
||||
};
|
||||
}
|
||||
|
||||
function checkDeps() {
|
||||
const pkgJson = glob
|
||||
.sync(['dist/node_modules/@*/*/package.json', 'dist/node_modules/*/package.json'])
|
||||
.sort()
|
||||
.map(path => fs.readFileSync(path, 'utf8'))
|
||||
.map(content => JSON.parse(content))
|
||||
.filter(({ author }) => JSON.stringify(author ?? 'null').includes('ljharb'))
|
||||
.map(({ name }) => name);
|
||||
|
||||
return { suspiciousPackages: pkgJson };
|
||||
}
|
||||
|
||||
console.log({
|
||||
...checkImports(),
|
||||
...checkDeps(),
|
||||
});
|
19
scripts/pull.sh
Executable file
19
scripts/pull.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
pull() {
|
||||
echo "🚛 Pulling $2"
|
||||
if [ ! -d "packages/$2" ]; then
|
||||
echo "📦 Repository not found, cloning..."
|
||||
git clone "https://github.com/$1/$2.git" "packages/$2"
|
||||
fi
|
||||
(cd "packages/$2" && git config pull.rebase true && git config rebase.autoStash true && git pull)
|
||||
echo
|
||||
}
|
||||
|
||||
pull import-js eslint-import-resolver-typescript
|
||||
pull import-js eslint-plugin-import
|
||||
pull jsx-eslint eslint-plugin-jsx-a11y
|
||||
pull eslint-community eslint-plugin-n
|
||||
pull jsx-eslint eslint-plugin-react
|
||||
pull jsx-eslint jsx-ast-utils
|
11
scripts/save_patch.sh
Executable file
11
scripts/save_patch.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
sync() (
|
||||
cd "packages/$1" && git diff HEAD > "../../patch/$1.patch"
|
||||
)
|
||||
|
||||
sync eslint-import-resolver-typescript
|
||||
sync eslint-plugin-import
|
||||
sync eslint-plugin-jsx-a11y
|
||||
sync eslint-plugin-n
|
||||
sync eslint-plugin-react
|
||||
sync jsx-ast-utils
|
Reference in New Issue
Block a user