#!/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(); 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 { filter( predicate: BooleanConstructor, ): Exclude[]; } } 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: {}, alias: { // esm modules 'find-cache-dir': require.resolve('find-cache-dir'), }, banner: { js: '/* eslint-disable */', }, }); // https://github.com/eslint-types/define-config-plugin-types/issues/32 // const distPackageJson = JSON.parse(await fs.readFile('./dist/package.json', 'utf-8')); // Object.assign(distPackageJson.dependencies, pkg.dependencies); // await fs.writeFile( // './dist/package.json', // JSON.stringify(distPackageJson, null, 2) + '\n', // ); } 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', './dist/eslint-plugin-n/index.js'); main('./packages/eslint-import-resolver-typescript/src/index.ts'); main('./src/rules/index.ts', './dist/eslint-plugin-rules/index.js'); main('./src/local/index.ts', './dist/eslint-plugin-local/index.js'); main('./src/index.ts', './dist/index.js');