import { DefinePlugin, IgnorePlugin, type Configuration } from "webpack"; import { findNatives } from "./src/meta/pack/natives.ts"; import { existingSources } from "./src/meta/pack/sources.ts"; import CopyPlugin from "copy-webpack-plugin"; import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; import { readdir } from "node:fs/promises"; const self = new URL(import.meta.url).pathname; function file(path: string): string { return new URL(path, import.meta.url).pathname; } export default async (): Promise => { // global webpack config options const defines: Record = {}; const copies: CopyPlugin.Pattern[] = []; const aliases: Record = {}; const compiledSources: string[] = []; // read environment to configure the build const env = process.env.NODE_ENV === "production" ? "production" : "development"; const dbSources = process.env.DB_SOURCES === "dynamic" ? "dynamic" : process.env.DB_SOURCES === "static" ? "static" : "shared"; let disableDbSqlite = process.env.DISABLE_DB_SQLITE === "1"; let disableBs3 = disableDbSqlite || process.env.DISABLE_BS3 === "1"; let disableNodeSqlite = disableDbSqlite || process.env.DISABLE_NODE_SQLITE === "1"; const disableDbApi = process.env.DISABLE_DB_API === "1"; // find natives and add them to the build const natives: Record = await findNatives(); if (disableBs3) natives.sqlite = null; if (!natives.sqlite) disableBs3 = true; for (const [k, v] of Object.entries(natives)) { if (!v) { defines[`natives.${k}`] = "null"; continue; } defines[`natives.${k}`] = "(__dirname+" + JSON.stringify("/../natives/" + v.split("/").at(-1)!) + ")"; copies.push({ from: v, to: "./natives" }); } // find binaries and add them to the build const binaries = Object.fromEntries( (await readdir(file("./src/bin"))).map((bin) => [ bin.replace(/\..+$/, ""), file(`./src/bin/${bin}`), ]) ); for (const bin of Object.keys(binaries)) { copies.push({ from: self, to: `./bin/${bin}`, toType: "file", transform: () => `#!/bin/sh\nexec node --enable-source-maps "$(dirname "$0")/${bin}.cjs" "$@"\n`, }); } // find schemas and add them to the build const schemas: string[] = []; { async function impl(dir: string) { for (const file of await readdir(dir, { withFileTypes: true })) { if (file.isDirectory()) { await impl(dir + "/" + file.name); } else if (file.isFile() && file.name.endsWith(".schema.json")) { schemas.push(dir + "/" + file.name); } } } await impl(file("./src/schema")); for (const schema of schemas) { copies.push({ from: schema, to: "./schema" }); } } // resolve db sources statically or dynamically if (dbSources === "dynamic") { console.log("Resolving db interfaces dynamically"); aliases[file("./src/db/dbSources.ts")] = file("./src/db/dbSources.dyn.ts"); } else if (dbSources === "static") { console.log("Resolving db interfaces statically"); aliases[file("./src/db/dbSources.ts")] = file( "./src/db/dbSources.static.ts" ); } else { console.log("Resolving db interfaces shared"); aliases[file("./src/db/dbSources.ts")] = file( "./src/db/dbSources.shared.ts" ); } // disable parts or all of the db interface if (disableBs3 && disableNodeSqlite) disableDbSqlite = true; if (disableDbSqlite) { console.log("Disabling sqlite db interface completely"); aliases[file("./src/db/sqlite/getdb.static.ts")] = file("./src/db/stub.ts"); aliases[file("./src/db/sqlite/getdb.dyn.ts")] = file("./src/db/stub.ts"); } else if (disableBs3) { console.log("Disabling better-sqlite3 sqlite db backend"); aliases[file("./src/db/sqlite/impl/implementations.ts")] = file( "./src/db/sqlite/impl/implementations.node.ts" ); compiledSources.push("sqlite"); } else if (disableNodeSqlite) { console.log("Disabling node:sqlite sqlite db backend"); aliases[file("./src/db/sqlite/impl/implementations.ts")] = file( "./src/db/sqlite/impl/implementations.bs3.ts" ); compiledSources.push("sqlite"); } else { console.log( "Enabling sqlite db interface with better-sqlite3 and node:sqlite backends" ); aliases[file("./src/db/sqlite/impl/implementations.ts")] = file( "./src/db/sqlite/impl/implementations.all.ts" ); compiledSources.push("sqlite"); } // disable the api db interface if (disableDbApi) { console.log("Disabling api db interface completely"); aliases[file("./src/db/api/getdb.static.ts")] = file("./src/db/stub.ts"); aliases[file("./src/db/api/getdb.dyn.ts")] = file("./src/db/stub.ts"); } else { console.log("Enabling api db interface"); compiledSources.push("api"); } // check the compiled sources and add it to the defines compiledSources.sort(); defines.compiledSources = JSON.stringify(compiledSources); for (const source of existingSources) defines[`compiledSources.${source}`] = JSON.stringify( compiledSources.includes(source) ); if (!compiledSources.length) { console.warn( "No db interface enabled, the builds will be completely useless" ); process.exit(1); } // log about the natives we have console.log( `Using ${Object.values(natives).filter(Boolean).length} natives:` ); for (const [key, path] of Object.entries(natives)) { if (path) console.log(`- ${key}: ${path}`); else console.warn(`- ${key}: (not found)`); } // log about the binaries we're making console.log(`Generating ${Object.entries(binaries).length} binaries:`); for (const [key, path] of Object.entries(binaries)) { console.log(`- ${key}: ${path}`); } // generate a json report for the stuff included in the build { const report = { binaries: Object.keys(binaries), natives: Object.entries(natives) .filter((x) => x[1]) .map((x) => x[0]), db: { mode: dbSources, sources: compiledSources }, schemas: schemas.map((x) => x.split("/").at(-1)!), }; if (!disableDbSqlite) Object.assign(report.db, { sqlite: { bs3: !disableBs3, node: !disableNodeSqlite }, }); copies.push({ from: self, to: "./report/build.json", toType: "file", transform: () => JSON.stringify(report, null, 2), }); } const config: Configuration[] = []; config.push({ mode: env, devtool: "source-map", target: "node", entry: binaries, output: { path: file("./dist"), filename: "bin/[name].cjs", chunkFilename: "chunk/bin.[name].chunk.cjs", }, resolve: { extensionAlias: { ".js": [".js", ".ts", ".tsx"], }, extensions: [".js", ".ts"], alias: { ...aliases, }, }, module: { rules: [ { test: /\.sql$/, include: file("./src"), loader: "raw-loader", }, { type: "javascript/esm", include: file("./src/schema/validators.ts"), use: [ { loader: "val-loader", options: { executableFile: file("./src/meta/pack/valLoader.ts"), loader: file("./src/meta/pack/validators.ts"), }, }, ], }, { test: /\.tsx?$/, type: "javascript/esm", include: file("./src"), use: [ { loader: "ts-loader", options: { ignoreDiagnostics: true, compilerOptions: { noEmit: false, jsx: "react-jsx", diagnostics: false, noEmitOnError: false, }, }, }, ], }, ], }, plugins: [ new DefinePlugin({ ...defines, "import.meta.hot": "undefined", }), new IgnorePlugin({ resourceRegExp: /^react-devtools-core$|^\.\/devtools\.js$|^bindings$/, }), new CopyPlugin({ patterns: copies, }), new BundleAnalyzerPlugin({ openAnalyzer: false, analyzerMode: "static", reportFilename: "report/bin.html", }), ], ignoreWarnings: [ { // webpack doesn't like the `require` used by better-sqlite3 // it's an indirect call to a runtime string, so it can't be analyzed // the way we use it however, we force the argument to be constant at build time module: /\/node_modules\/better-sqlite3\/lib\/database\.js/, }, ], }); return config; };