#!/usr/bin/env -S node -r esbin import "dotenv/config" import { promises as fs } from "fs" import { resolve } from "path" import glob from "fast-glob" import { program } from "commander" import sass from "esbuild-plugin-sass" import * as esbuild from "esbuild" import type { PluginItem } from "@babel/core" import { c } from "./utils" import { cssModules } from "./plugins/esbuild-css-modules" import { babelPlugin as babel } from "./plugins/esbuild-babel" import { externalDep } from "./plugins/esbuild-external-dep" import { stringImport } from "./plugins/esbuild-string-import" import { yamlPlugin as yaml } from "./plugins/esbuild-yaml" import { alias } from "./plugins/esbuild-alias" import { getDefaultTarget } from "./plugins/esbuild-browserslist" const dist = "dist" const SERVE_PORT = 3114 program .option("-g, --grep ", "only build packages that match ") .option("-s, --silent", "hide all output") .option( "-e, --env ", "set the NODE_ENV environment variable", process.env.NODE_ENV || "production", ) .option("-w, --watch", "watch for changes and rebuild") .parse() const { grep, silent, env, watch } = program.opts() const __PROD__ = env === "production" const log = silent ? () => {} : console.log async function main() { const queue = new Queue([ new Build("src/options/index.tsx", `${dist}/app/options`, { serve: true }), new Build("src/popup/index.tsx", `${dist}/app/popup`), new Build("src/server/index.ts", `${dist}/app/server`, { platform: "node", format: "cjs", splitting: false, minify: false, }), new Build("src/background/index.ts", `${dist}/app/background`), new Build("src/background/compiler.ts", `${dist}/app/background/compiler`), new Build("src/context/index.ts", `${dist}/app/context`, { format: "iife", splitting: false, }), new Build("src/devtools/index.ts", `${dist}/app/devtools`), new Build("src/install/index.ts", `${dist}/app/install`), new Build("src/graphiql/index.tsx", `${dist}/app/playground`), new Build("src/reference/index.tsx", `${dist}/app/reference`, { format: "iife", splitting: false, }), ]).filter(grep ? task => task.entry.includes(grep) : () => true) if (!queue.length) { return } await queue.run(task => task.clean()) log( watch ? "Watching" : "Building", `${c.blue(queue.length)} package(s) in ${c.blue(env)}`, ) if (queue.length < 5) { log(queue.tasks.map((t, i) => ` ${i + 1}. ${c.green(t.entry)}`).join("\n")) } await queue.build() if (__PROD__) { await printBundleSize() } } if (require.main === module) { Promise.resolve() .then(() => main()) .catch(e => { console.error(e) process.exit(1) }) } type BuildOptions = Pick< esbuild.BuildOptions, "entryPoints" | "outdir" | "format" | "splitting" | "platform" | "minify" > & { serve?: boolean } export const getBasicOptions = ({ entryPoints, minify = __PROD__, plugins = [], babelPlugins = [], }: { entryPoints: [string] minify?: boolean plugins?: esbuild.Plugin[] babelPlugins?: PluginItem[] }): Partial => ({ entryPoints, define: { ...Object.entries(process.env).reduce( (acc, [key, value]) => key.startsWith("NEXT_PUBLIC_") ? { ...acc, [`process.env.${key}`]: JSON.stringify(value) } : acc, {} as Record, ), "process.env.NODE_ENV": JSON.stringify(env), "process.env.NODE_DEBUG": "false", "process.env.SERVE_PORT": String(SERVE_PORT), "process.browser": "true", global: "globalThis", }, bundle: true, format: "esm", external: ["path", "glob", "fs", "util", "monaco-editor"], plugins: [ ...plugins, externalDep(["prettier", "sass"]), babel(babelPlugins), yaml(), stringImport(), alias({ lodash: require.resolve("lodash-es"), "webextension-polyfill": require.resolve("../src/vendor/webextension-polyfill"), "monaco-editor": require.resolve("../src/options/monaco.ts"), // https://github.com/MichalLytek/type-graphql/issues/366#issuecomment-511075437 "libphonenumber-js": require.resolve("lodash.noop"), // we never use graphql-subscriptions "graphql-subscriptions": require.resolve("lodash.noop"), // Make esbuild use the module version graphql: require.resolve("graphql/index.mjs"), ...(entryPoints[0].includes("graphiql") || __PROD__ ? {} : { react: require.resolve("../src/vendor/why-did-you-render/index.js"), "react/jsx-runtime": require.resolve( "../src/vendor/why-did-you-render/jsx-runtime", ), }), }), cssModules({ generateScopedName: __PROD__ ? "[hash:base64:6]" : "[name]__[local]___[hash:base64:5]", localsConvention: "camelCaseOnly", }), sass(), ].filter(Boolean), target: getDefaultTarget(), banner: { js: "/* eslint-disable */", }, legalComments: "none", keepNames: false, tsconfig: "./tsconfig.json", sourcemap: "linked", minify, splitting: true, metafile: true, loader: { ".eot": "file", ".png": "file", ".ttf": "file", ".woff": "file", ".woff2": "file", }, }) async function printBundleSize() { const root = resolve(dist, "app") const files = await glob(["**/*", "!**/*.map", "!**/meta.json"], { cwd: root, onlyFiles: true, }) const sizes = await Promise.all( files.map(async file => { const { size } = await fs.stat(resolve(root, file)) return { file, size } }), ) const list = sizes .filter(a => a.size > 50000) .sort((a, b) => b.size - a.size) .slice(0, 10) for (const { file, size } of list) { log( c.blue(`./dist/app/${file.padEnd(38, " ")}`), c.green(`${humanFileSize(size).padStart(9, " ")}`), ) } } function humanFileSize(bytes: number, si = false, dp = 1) { const thresh = si ? 1000 : 1024 if (Math.abs(bytes) < thresh) { return bytes + " B" } const units = si ? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] : ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] let u = -1 const r = 10 ** dp do { bytes /= thresh ++u } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1) return bytes.toFixed(dp) + " " + units[u] } class Build { enabled = true constructor( readonly entry: string, readonly outdir: string, readonly options?: BuildOptions, ) {} disable() { this.enabled = false return this } async clean() { if (!this.enabled) return await fs.rm(this.outdir, { recursive: true, force: true }) } async build() { if (!this.enabled) return const { outdir, options: { serve, ...options } = {} } = this const ctx = await esbuild.context({ ...getBasicOptions({ entryPoints: [this.entry], }), ...options, outdir, }) const { metafile } = await ctx.rebuild() if (__PROD__) { await fs.writeFile(resolve(outdir, "meta.json"), JSON.stringify(metafile)) } if (watch) { await ctx.watch() if (serve) { await ctx.serve({ port: SERVE_PORT }) } } else { await ctx.dispose() } } } class Queue { constructor(public tasks: readonly Build[]) {} get length() { return this.tasks.length } filter(predicate: (task: Build) => boolean) { return new Queue(this.tasks.filter(predicate)) } run(fn: (task: Build) => Promise) { return Promise.all(this.tasks.map(fn)) } build() { return this.run(task => task.build()) } }