stylebot-harmony/scripts/build-apps.ts
2023-08-03 20:09:32 -04:00

301 lines
7.5 KiB
JavaScript
Executable File

#!/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 <pattern>", "only build packages that match <pattern>")
.option("-s, --silent", "hide all output")
.option(
"-e, --env <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<esbuild.BuildOptions> => ({
entryPoints,
define: {
...Object.entries(process.env).reduce(
(acc, [key, value]) =>
key.startsWith("NEXT_PUBLIC_")
? { ...acc, [`process.env.${key}`]: JSON.stringify(value) }
: acc,
{} as Record<string, string>,
),
"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<void>) {
return Promise.all(this.tasks.map(fn))
}
build() {
return this.run(task => task.build())
}
}