diff --git a/source/build.ts b/source/build.ts new file mode 100644 index 0000000..166824f --- /dev/null +++ b/source/build.ts @@ -0,0 +1,82 @@ +// Import native Node libraries. +import path from "node:path"; +import process from "node:process"; +import fsp from "node:fs/promises"; +// Import Esbuild and associated plugins. +import esbuild from "esbuild"; +import copyPlugin from "esbuild-copy-static-files"; +import {sassPlugin} from "esbuild-sass-plugin"; +// Import PostCSS and associated plugins. +import cssnano from "cssnano"; +import postcss from "postcss"; + +/** + * Create an absolute path from a given relative one, using the directory + * this file is located in as the base. + * + * @param relative The relative path to make absolute. + * @returns The absolute path. + */ +function toAbsolutePath(relative: string): string { + return new URL(relative, import.meta.url).pathname; +} + +// Create variables based on the environment. +const dev = process.env.NODE_ENV === "development"; +const watch = process.env.WATCH === "true"; + +// Create absolute paths to various directories. +const outDir = toAbsolutePath("../build"); +const sourceDir = toAbsolutePath("../source"); + +// Ensure that the output directory exists. +await fsp.mkdir(outDir, {recursive: true}); + +const cssProcessor = postcss([cssnano()]); + +const options: esbuild.BuildOptions = { + bundle: true, + // Define variables to be replaced in the code. Note that these are replaced + // "as is" and so we have to stringify them as JSON, otherwise a string won't + // have its quotes for example. + define: { + $dev: JSON.stringify(dev), + }, + entryPoints: [path.join(sourceDir, "setup.tsx")], + format: "esm", + logLevel: "info", + minify: !dev, + outdir: outDir, + plugins: [ + // Copy all files from `source/assets/` to the output directory. + copyPlugin({src: path.join(sourceDir, "assets/"), dest: outDir}), + + // Compile SCSS to CSS. + sassPlugin({ + type: "style", + async transform(source) { + // In development, don't do any extra processing. + if (dev) { + return source; + } + + // But in production, run the CSS through PostCSS. + const {css} = await cssProcessor.process(source, {from: undefined}); + return css; + }, + }), + ], + // Link sourcemaps in development but omit them in production. + sourcemap: dev ? "linked" : false, + splitting: true, + // Target ES2022, and the first Chromium and Firefox releases from 2022. + target: ["es2022", "chrome97", "firefox102"], + treeShaking: true, +}; + +if (watch) { + const context = await esbuild.context(options); + await context.watch(); +} else { + await esbuild.build(options); +} diff --git a/source/packages.d.ts b/source/packages.d.ts new file mode 100644 index 0000000..4311bd7 --- /dev/null +++ b/source/packages.d.ts @@ -0,0 +1,15 @@ +// Type definitions for third-party packages. + +declare module "esbuild-copy-static-files" { + import {type cpSync} from "node:fs"; + import {type Plugin} from "esbuild"; + + type CopySyncParameters = Parameters; + + type Options = { + src?: CopySyncParameters[0]; + dest?: CopySyncParameters[1]; + } & CopySyncParameters[2]; + + export default function (options: Options): Plugin; +} diff --git a/source/types.d.ts b/source/types.d.ts new file mode 100644 index 0000000..638f608 --- /dev/null +++ b/source/types.d.ts @@ -0,0 +1,6 @@ +// Export something so TypeScript doesn't see this file as an ambient module. +export {}; + +declare global { + const $dev: boolean; +}