eslint-rules/esbuild.ts
2023-11-01 21:14:15 -04:00

278 lines
7.5 KiB
TypeScript
Executable File

#!/usr/bin/env tsx
import assert from 'node:assert';
import { readFileSync } 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';
const polyfills = Object.keys(polyfill);
const ENV = process.env.NODE_ENV || 'development';
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(
'string.prototype.matchall',
proto(t => t.identifier('matchAll')),
)
.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)),
);
}
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 main(
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: {},
banner: {
js: '/* eslint-disable */',
},
});
}
main('./packages/eslint-plugin-react/index.js');
main('./packages/eslint-plugin-import/src/index.js');
main('./packages/eslint-plugin-jsx-a11y/src/index.js');
main('./packages/eslint-plugin-react-hooks/index.ts');
main('./packages/eslint-plugin-n/lib/index.js');
main('./packages/eslint-import-resolver-typescript/src/index.ts');
main('./src/rules/index.ts', './dist/rules/index.js');
main('./src/index.ts', './dist/index.js');