#!/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(); 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 { 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 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();