diff --git a/tools/lib/build.ts b/tools/lib/build.ts index 0c9cbbde3..4aa184a57 100644 --- a/tools/lib/build.ts +++ b/tools/lib/build.ts @@ -1,14 +1,12 @@ import fs from "node:fs/promises"; -import { - buildCSS, - copyFiles, - getSport, - minifyIndexHTML, - reset, -} from "./buildFuncs.ts"; -import generateJSONSchema from "./generateJSONSchema.ts"; -import buildJS from "./build-js.ts"; -import buildSW from "./build-sw.ts"; +import { buildCss } from "./buildCss.ts"; +import { buildJs } from "./buildJs.ts"; +import { buildSw } from "./buildSw.ts"; +import { copyFiles } from "./copyFiles.ts"; +import { generateJsonSchema } from "./generateJsonSchema.ts"; +import { getSport } from "./getSport.ts"; +import { minifyIndexHtml } from "./minifyIndexHtml.ts"; +import { reset } from "./reset.ts"; export default async () => { const sport = getSport(); @@ -18,7 +16,7 @@ export default async () => { await reset(); await copyFiles(); - const jsonSchema = generateJSONSchema(sport); + const jsonSchema = generateJsonSchema(sport); await fs.mkdir("build/files", { recursive: true }); await fs.writeFile( "build/files/league-schema.json", @@ -26,12 +24,12 @@ export default async () => { ); console.log("Bundling JavaScript files..."); - await buildJS(); + await buildJs(); console.log("Processing CSS/HTML files..."); - await buildCSS(); - await minifyIndexHTML(); + await buildCss(); + await minifyIndexHtml(); console.log("Generating sw.js..."); - await buildSW(); + await buildSw(); }; diff --git a/tools/lib/buildCss.ts b/tools/lib/buildCss.ts new file mode 100644 index 000000000..85c674917 --- /dev/null +++ b/tools/lib/buildCss.ts @@ -0,0 +1,104 @@ +import { Buffer } from "node:buffer"; +import fs from "node:fs"; +import browserslist from "browserslist"; +import * as lightningCSS from "lightningcss"; +import { PurgeCSS } from "purgecss"; +import * as sass from "sass"; +import { fileHash } from "./fileHash.ts"; +import { replace } from "./replace.ts"; + +export const buildCss = async (watch: boolean = false) => { + const filenames = ["light", "dark"]; + const rawCSS = filenames.map((filename) => { + const sassFilePath = `public/css/${filename}.scss`; + const sassResult = sass.renderSync({ + file: sassFilePath, + }); + return sassResult.css.toString(); + }); + + const purgeCSSResults = watch + ? [] + : await new PurgeCSS().purge({ + content: ["build/gen/*.js"], + css: rawCSS.map((raw) => ({ raw })), + safelist: { + standard: [/^qc-cmp2-persistent-link$/], + greedy: [ + // react-bootstrap stuff + /^modal/, + /^navbar/, + /^popover/, + /^tooltip/, + /^bs-tooltip/, + + // For align="end" in react-bootstrap + /^dropdown-menu-end$/, + + // flag-icons + /^fi$/, + /^fi-/, + + /^dark-select/, + /^bar-graph/, + /^watch-active/, + /^dashboard-top-link-other/, + ], + }, + }); + + for (let i = 0; i < filenames.length; i++) { + const filename = filenames[i]; + + let output; + if (!watch) { + // https://zengm.com/blog/2022/07/investigating-a-tricky-performance-bug/ + const DANGER_CSS = ".input-group.has-validation"; + if (!rawCSS[i].includes(DANGER_CSS)) { + throw new Error( + `rawCSS no longer contains ${DANGER_CSS} - same problem might exist with another name?`, + ); + } + + const purgeCSSResult = purgeCSSResults[i].css; + + const { code } = lightningCSS.transform({ + filename: `${filename}.css`, + code: Buffer.from(purgeCSSResult), + minify: true, + sourceMap: false, + targets: lightningCSS.browserslistToTargets( + browserslist("Chrome >= 75, Firefox >= 78, Safari >= 12.1"), + ), + }); + + output = code.toString(); + + if (output.includes(DANGER_CSS)) { + throw new Error(`CSS output contains ${DANGER_CSS}`); + } + } else { + output = rawCSS[i]; + } + + let outFilename; + if (watch) { + outFilename = `build/gen/${filename}.css`; + } else { + const hash = fileHash(output); + outFilename = `build/gen/${filename}-${hash}.css`; + + replace({ + paths: ["build/index.html"], + replaces: [ + { + searchValue: `CSS_HASH_${filename.toUpperCase()}`, + replaceValue: hash, + }, + ], + }); + } + + fs.writeFileSync(outFilename, output); + } +}; diff --git a/tools/lib/buildFuncs.ts b/tools/lib/buildFuncs.ts deleted file mode 100644 index f417ceb25..000000000 --- a/tools/lib/buildFuncs.ts +++ /dev/null @@ -1,619 +0,0 @@ -import * as lightningCSS from "lightningcss"; -import browserslist from "browserslist"; -import { Buffer } from "node:buffer"; -import crypto from "node:crypto"; -import fs from "node:fs"; -import fsp from "node:fs/promises"; -import * as htmlmin from "html-minifier-terser"; -import * as sass from "sass"; -import { PurgeCSS } from "purgecss"; - -const SPORTS = ["baseball", "basketball", "football", "hockey"] as const; - -const getSport = () => { - if (SPORTS.includes(process.env.SPORT)) { - return process.env.SPORT; - } - if (process.env.SPORT === undefined) { - return "basketball"; - } - throw new Error(`Invalid SPORT: ${process.env.SPORT}`); -}; - -const fileHash = (contents: string) => { - // https://github.com/sindresorhus/rev-hash - return crypto.createHash("md5").update(contents).digest("hex").slice(0, 10); -}; - -const replace = ({ - paths, - replaces, -}: { - paths: fs.PathOrFileDescriptor[]; - replaces: { - searchValue: string | RegExp; - replaceValue: string; - }[]; -}) => { - for (const path of paths) { - let contents = fs.readFileSync(path, "utf8"); - for (const { searchValue, replaceValue } of replaces) { - contents = contents.replaceAll(searchValue, replaceValue); - } - fs.writeFileSync(path, contents); - } -}; - -const buildCSS = async (watch: boolean = false) => { - const filenames = ["light", "dark"]; - const rawCSS = filenames.map((filename) => { - const sassFilePath = `public/css/${filename}.scss`; - const sassResult = sass.renderSync({ - file: sassFilePath, - }); - return sassResult.css.toString(); - }); - - const purgeCSSResults = watch - ? [] - : await new PurgeCSS().purge({ - content: ["build/gen/*.js"], - css: rawCSS.map((raw) => ({ raw })), - safelist: { - standard: [/^qc-cmp2-persistent-link$/], - greedy: [ - // react-bootstrap stuff - /^modal/, - /^navbar/, - /^popover/, - /^tooltip/, - /^bs-tooltip/, - - // For align="end" in react-bootstrap - /^dropdown-menu-end$/, - - // flag-icons - /^fi$/, - /^fi-/, - - /^dark-select/, - /^bar-graph/, - /^watch-active/, - /^dashboard-top-link-other/, - ], - }, - }); - - for (let i = 0; i < filenames.length; i++) { - const filename = filenames[i]; - - let output; - if (!watch) { - // https://zengm.com/blog/2022/07/investigating-a-tricky-performance-bug/ - const DANGER_CSS = ".input-group.has-validation"; - if (!rawCSS[i].includes(DANGER_CSS)) { - throw new Error( - `rawCSS no longer contains ${DANGER_CSS} - same problem might exist with another name?`, - ); - } - - const purgeCSSResult = purgeCSSResults[i].css; - - const { code } = lightningCSS.transform({ - filename: `${filename}.css`, - code: Buffer.from(purgeCSSResult), - minify: true, - sourceMap: false, - targets: lightningCSS.browserslistToTargets( - browserslist("Chrome >= 75, Firefox >= 78, Safari >= 12.1"), - ), - }); - - output = code.toString(); - - if (output.includes(DANGER_CSS)) { - throw new Error(`CSS output contains ${DANGER_CSS}`); - } - } else { - output = rawCSS[i]; - } - - let outFilename; - if (watch) { - outFilename = `build/gen/${filename}.css`; - } else { - const hash = fileHash(output); - outFilename = `build/gen/${filename}-${hash}.css`; - - replace({ - paths: ["build/index.html"], - replaces: [ - { - searchValue: `CSS_HASH_${filename.toUpperCase()}`, - replaceValue: hash, - }, - ], - }); - } - - fs.writeFileSync(outFilename, output); - } -}; - -const bySport = ( - object: - | { - baseball: T; - basketball: T; - football: T; - hockey: T; - default?: T; - } - | { - baseball?: T; - basketball?: T; - football?: T; - hockey?: T; - default: T; - }, -): T => { - const sport = getSport(); - if (Object.hasOwn(object, sport)) { - return (object as any)[sport]; - } - - if (Object.hasOwn(object, "default")) { - return (object as any).default; - } - - throw new Error("No value for sport and no default"); -}; - -const setSport = () => { - replace({ - paths: ["build/index.html"], - replaces: [ - { - searchValue: "GAME_NAME", - replaceValue: bySport({ - baseball: "ZenGM Baseball", - basketball: "Basketball GM", - football: "Football GM", - hockey: "ZenGM Hockey", - }), - }, - { - searchValue: "SPORT", - replaceValue: bySport({ - baseball: "baseball", - basketball: "basketball", - football: "football", - hockey: "hockey", - }), - }, - { - searchValue: "GOOGLE_ANALYTICS_COOKIE_DOMAIN", - replaceValue: bySport({ - basketball: "basketball-gm.com", - football: "football-gm.com", - default: "zengm.com", - }), - }, - { - searchValue: "WEBSITE_ROOT", - replaceValue: bySport({ - baseball: "zengm.com/baseball", - basketball: "basketball-gm.com", - football: "football-gm.com", - hockey: "zengm.com/hockey", - }), - }, - { - searchValue: "PLAY_SUBDOMAIN", - replaceValue: bySport({ - baseball: "baseball.zengm.com", - basketball: "play.basketball-gm.com", - football: "play.football-gm.com", - hockey: "hockey.zengm.com", - }), - }, - { - searchValue: "BETA_SUBDOMAIN", - replaceValue: bySport({ - baseball: "beta.baseball.zengm.com", - basketball: "beta.basketball-gm.com", - football: "beta.football-gm.com", - hockey: "beta.hockey.zengm.com", - }), - }, - ], - }); -}; - -const copyFiles = async (watch: boolean = false) => { - const foldersToIgnore = [ - "baseball", - "basketball", - "css", - "football", - "hockey", - ]; - - await fsp.cp("public", "build", { - filter: (filename) => { - // Loop through folders to ignore. - for (const folder of foldersToIgnore) { - if (filename.startsWith(`public/${folder}`)) { - return false; - } - } - - // Remove service worker, so I don't have to deal with it being wonky in dev - if (watch && filename === "public/sw.js") { - return false; - } - - return true; - }, - recursive: true, - }); - - let sport = process.env.SPORT; - if (typeof sport !== "string") { - sport = "basketball"; - } - - await fsp.cp(`public/${sport}`, "build", { - filter: (filename) => !filename.includes(".gitignore"), - recursive: true, - }); - - // Remove the empty folders created by the "filter" function. - for (const folder of foldersToIgnore) { - await fsp.rm(`build/${folder}`, { recursive: true, force: true }); - } - - const realPlayerFilenames = ["real-player-data", "real-player-stats"]; - for (const filename of realPlayerFilenames) { - const sourcePath = `data/${filename}.${sport}.json`; - if (fs.existsSync(sourcePath)) { - await fsp.copyFile(sourcePath, `build/gen/${filename}.json`); - } - } - - await fsp.copyFile("data/names.json", "build/gen/names.json"); - await fsp.copyFile("data/names-female.json", "build/gen/names-female.json"); - - await fsp.cp("node_modules/flag-icons/flags/4x3", "build/img/flags", { - recursive: true, - }); - const flagHtaccess = ` - Header set Cache-Control "public,max-age=31536000" -`; - await fsp.writeFile("build/img/flags/.htaccess", flagHtaccess); - - setSport(); -}; - -const genRev = () => { - const date = new Date(); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - const minutes = String(date.getMinutes() + 60 * date.getHours()).padStart( - 4, - "0", - ); - - return `${year}.${month}.${day}.${minutes}`; -}; - -const reset = async () => { - await fsp.rm("build", { recursive: true, force: true }); - await fsp.mkdir("build/gen", { recursive: true }); -}; - -const setTimestamps = (rev: string, watch: boolean = false) => { - if (watch) { - replace({ - paths: ["build/index.html"], - replaces: [ - { - searchValue: "-REV_GOES_HERE.js", - replaceValue: ".js", - }, - { - searchValue: '-" + bbgmVersion + "', - replaceValue: "", - }, - { - searchValue: /-CSS_HASH_(LIGHT|DARK)/g, - replaceValue: "", - }, - { - searchValue: "REV_GOES_HERE", - replaceValue: rev, - }, - ], - }); - } else { - replace({ - paths: [ - "build/index.html", - - // This is currently just for lastChangesVersion, so don't worry about it not working in watch mode - `build/gen/worker-${rev}.js`, - `build/gen/worker-legacy-${rev}.js`, - ], - replaces: [ - { - searchValue: "REV_GOES_HERE", - replaceValue: rev, - }, - ], - }); - } - - // Quantcast Choice. Consent Manager Tag v2.0 (for TCF 2.0) - const bannerAdsCode = ` - - - - - - - - - - -`; - - if (!watch) { - replace({ - paths: [`build/gen/ui-legacy-${rev}.js`], - replaces: [ - { - searchValue: "/gen/worker-", - replaceValue: "/gen/worker-legacy-", - }, - ], - }); - } - - replace({ - paths: ["build/index.html"], - replaces: [ - { - searchValue: "BANNER_ADS_CODE", - replaceValue: bannerAdsCode, - }, - { - searchValue: "GOOGLE_ANALYTICS_ID", - replaceValue: bySport({ - basketball: "UA-38759330-1", - football: "UA-38759330-2", - default: "UA-38759330-3", - }), - }, - { - searchValue: "BUGSNAG_API_KEY", - replaceValue: bySport({ - baseball: "37b1fd32d021f7716dc0e1d4a3e619bc", - basketball: "c10b95290070cb8888a7a79cc5408555", - football: "fed8957cbfca2d1c80997897b840e6cf", - hockey: "449e8ed576f7cbccf5c7649e936ab9ff", - }), - }, - ], - }); - - return rev; -}; - -const minifyIndexHTML = async () => { - const content = fs.readFileSync("build/index.html", "utf8"); - const minified = await htmlmin.minify(content, { - collapseBooleanAttributes: true, - collapseWhitespace: true, - minifyCSS: true, - minifyJS: true, - removeComments: true, - useShortDoctype: true, - }); - fs.writeFileSync("build/index.html", minified); -}; - -export { - bySport, - buildCSS, - copyFiles, - fileHash, - genRev, - getSport, - replace, - reset, - setTimestamps, - minifyIndexHTML, -}; diff --git a/tools/lib/build-js.ts b/tools/lib/buildJs.ts similarity index 77% rename from tools/lib/build-js.ts rename to tools/lib/buildJs.ts index df653ae7f..92a2b3a68 100644 --- a/tools/lib/build-js.ts +++ b/tools/lib/buildJs.ts @@ -1,11 +1,14 @@ import fs from "node:fs"; -import { fileHash, genRev, replace, setTimestamps } from "./buildFuncs.ts"; import { Worker } from "node:worker_threads"; +import { fileHash } from "./fileHash.ts"; +import { generateVersionNumber } from "./generateVersionNumber.ts"; +import { replace } from "./replace.ts"; +import { setTimestamps } from "./setTimestamps.ts"; -const rev = genRev(); -console.log(rev); +const versionNumber = generateVersionNumber(); +console.log(versionNumber); -const buildJS = async () => { +export const buildJs = async () => { const promises = []; for (const name of ["ui", "worker"]) { for (const legacy of [false, true]) { @@ -17,7 +20,7 @@ const buildJS = async () => { workerData: { legacy, name, - rev, + rev: versionNumber, }, }, ); @@ -46,7 +49,7 @@ const buildJS = async () => { ], }); - setTimestamps(rev); + setTimestamps(versionNumber); const jsonFiles = [ "names", @@ -73,9 +76,10 @@ const buildJS = async () => { } } replace({ - paths: [`build/gen/worker-legacy-${rev}.js`, `build/gen/worker-${rev}.js`], + paths: [ + `build/gen/worker-legacy-${versionNumber}.js`, + `build/gen/worker-${versionNumber}.js`, + ], replaces, }); }; - -export default buildJS; diff --git a/tools/lib/build-sw.ts b/tools/lib/buildSw.ts similarity index 84% rename from tools/lib/build-sw.ts rename to tools/lib/buildSw.ts index ec25749a5..79715bda5 100644 --- a/tools/lib/build-sw.ts +++ b/tools/lib/buildSw.ts @@ -5,18 +5,18 @@ import resolve from "@rollup/plugin-node-resolve"; import terser from "@rollup/plugin-terser"; import { rollup } from "rollup"; import workboxBuild from "workbox-build"; -import { replace as replace2 } from "./buildFuncs.ts"; +import { replace as replace2 } from "./replace.ts"; -const getRev = () => { +const getVersionNumber = () => { const files = fs.readdirSync("build/gen"); for (const file of files) { if (file.endsWith(".js")) { - const rev = file.split("-")[1].replace(".js", ""); - return rev; + const versionNumber = file.split("-")[1].replace(".js", ""); + return versionNumber; } } - throw new Error("rev not found"); + throw new Error("versionNumber not found"); }; // NOTE: This should be run *AFTER* all assets are built @@ -80,20 +80,18 @@ const bundle = async () => { }); }; -const buildSW = async () => { +export const buildSw = async () => { await injectManifest(); await bundle(); - const rev = getRev(); + const versionNumber = getVersionNumber(); replace2({ paths: ["build/sw.js"], replaces: [ { searchValue: "REV_GOES_HERE", - replaceValue: rev, + replaceValue: versionNumber, }, ], }); }; - -export default buildSW; diff --git a/tools/lib/bySport.ts b/tools/lib/bySport.ts new file mode 100644 index 000000000..75d3d6054 --- /dev/null +++ b/tools/lib/bySport.ts @@ -0,0 +1,30 @@ +import { getSport } from "./getSport.ts"; + +export const bySport = ( + object: + | { + baseball: T; + basketball: T; + football: T; + hockey: T; + default?: T; + } + | { + baseball?: T; + basketball?: T; + football?: T; + hockey?: T; + default: T; + }, +): T => { + const sport = getSport(); + if (Object.hasOwn(object, sport)) { + return (object as any)[sport]; + } + + if (Object.hasOwn(object, "default")) { + return (object as any).default; + } + + throw new Error("No value for sport and no default"); +}; diff --git a/tools/lib/copyFiles.ts b/tools/lib/copyFiles.ts new file mode 100644 index 000000000..ee1dfd549 --- /dev/null +++ b/tools/lib/copyFiles.ts @@ -0,0 +1,130 @@ +import fs from "node:fs"; +import fsp from "node:fs/promises"; +import { bySport } from "./bySport.ts"; +import { replace } from "./replace.ts"; + +const setSport = () => { + replace({ + paths: ["build/index.html"], + replaces: [ + { + searchValue: "GAME_NAME", + replaceValue: bySport({ + baseball: "ZenGM Baseball", + basketball: "Basketball GM", + football: "Football GM", + hockey: "ZenGM Hockey", + }), + }, + { + searchValue: "SPORT", + replaceValue: bySport({ + baseball: "baseball", + basketball: "basketball", + football: "football", + hockey: "hockey", + }), + }, + { + searchValue: "GOOGLE_ANALYTICS_COOKIE_DOMAIN", + replaceValue: bySport({ + basketball: "basketball-gm.com", + football: "football-gm.com", + default: "zengm.com", + }), + }, + { + searchValue: "WEBSITE_ROOT", + replaceValue: bySport({ + baseball: "zengm.com/baseball", + basketball: "basketball-gm.com", + football: "football-gm.com", + hockey: "zengm.com/hockey", + }), + }, + { + searchValue: "PLAY_SUBDOMAIN", + replaceValue: bySport({ + baseball: "baseball.zengm.com", + basketball: "play.basketball-gm.com", + football: "play.football-gm.com", + hockey: "hockey.zengm.com", + }), + }, + { + searchValue: "BETA_SUBDOMAIN", + replaceValue: bySport({ + baseball: "beta.baseball.zengm.com", + basketball: "beta.basketball-gm.com", + football: "beta.football-gm.com", + hockey: "beta.hockey.zengm.com", + }), + }, + ], + }); +}; + +export const copyFiles = async (watch: boolean = false) => { + const foldersToIgnore = [ + "baseball", + "basketball", + "css", + "football", + "hockey", + ]; + + await fsp.cp("public", "build", { + filter: (filename) => { + // Loop through folders to ignore. + for (const folder of foldersToIgnore) { + if (filename.startsWith(`public/${folder}`)) { + return false; + } + } + + // Remove service worker, so I don't have to deal with it being wonky in dev + if (watch && filename === "public/sw.js") { + return false; + } + + return true; + }, + recursive: true, + }); + + let sport = process.env.SPORT; + if (typeof sport !== "string") { + sport = "basketball"; + } + + await fsp.cp(`public/${sport}`, "build", { + filter: (filename) => !filename.includes(".gitignore"), + recursive: true, + }); + + // Remove the empty folders created by the "filter" function. + for (const folder of foldersToIgnore) { + await fsp.rm(`build/${folder}`, { recursive: true, force: true }); + } + + const realPlayerFilenames = ["real-player-data", "real-player-stats"]; + for (const filename of realPlayerFilenames) { + const sourcePath = `data/${filename}.${sport}.json`; + if (fs.existsSync(sourcePath)) { + await fsp.copyFile(sourcePath, `build/gen/${filename}.json`); + } + } + + await fsp.copyFile("data/names.json", "build/gen/names.json"); + await fsp.copyFile("data/names-female.json", "build/gen/names-female.json"); + + await fsp.cp("node_modules/flag-icons/flags/4x3", "build/img/flags", { + recursive: true, + }); + const flagHtaccess = ` + Header set Cache-Control "public,max-age=31536000" +`; + await fsp.writeFile("build/img/flags/.htaccess", flagHtaccess); + + setSport(); +}; diff --git a/tools/lib/deploy.ts b/tools/lib/deploy.ts index 191e969ca..07f353154 100644 --- a/tools/lib/deploy.ts +++ b/tools/lib/deploy.ts @@ -2,7 +2,8 @@ import { spawn } from "node:child_process"; import Cloudflare from "cloudflare"; import { readFile } from "node:fs/promises"; import build from "./build.ts"; -import { bySport, getSport } from "./buildFuncs.ts"; +import { bySport } from "./bySport.ts"; +import { getSport } from "./getSport.ts"; const getSubdomain = () => { const inputSubdomain = process.argv[2]; diff --git a/tools/lib/esbuildConfig.ts b/tools/lib/esbuildConfig.ts index 5a79b2169..333bf753f 100644 --- a/tools/lib/esbuildConfig.ts +++ b/tools/lib/esbuildConfig.ts @@ -4,7 +4,7 @@ import babel from "@babel/core"; import babelPluginSyntaxTypescript from "@babel/plugin-syntax-typescript"; // @ts-expect-error import { babelPluginSportFunctions } from "../babel-plugin-sport-functions/index.js"; -import { getSport } from "./buildFuncs.ts"; +import { getSport } from "./getSport.ts"; // Result is undefined if no match, meaning just do normal stuff type BabelCacheResult = diff --git a/tools/lib/fileHash.ts b/tools/lib/fileHash.ts new file mode 100644 index 000000000..2b5354089 --- /dev/null +++ b/tools/lib/fileHash.ts @@ -0,0 +1,6 @@ +import crypto from "node:crypto"; + +export const fileHash = (contents: string) => { + // https://github.com/sindresorhus/rev-hash + return crypto.createHash("md5").update(contents).digest("hex").slice(0, 10); +}; diff --git a/tools/lib/generateJSONSchema.ts b/tools/lib/generateJsonSchema.ts similarity index 99% rename from tools/lib/generateJSONSchema.ts rename to tools/lib/generateJsonSchema.ts index 9d2231691..427eba5bd 100644 --- a/tools/lib/generateJSONSchema.ts +++ b/tools/lib/generateJsonSchema.ts @@ -1,4 +1,4 @@ -import { bySport } from "./buildFuncs.ts"; +import { bySport } from "./bySport.ts"; const genRatings = (sport: string) => { const properties: any = { @@ -152,7 +152,7 @@ const wrap = (child: any) => ({ ], }); -const generateJSONSchema = (sport: string) => { +export const generateJsonSchema = (sport: string) => { if (sport === "test") { return { $schema: "http://json-schema.org/draft-07/schema#", @@ -2720,5 +2720,3 @@ const generateJSONSchema = (sport: string) => { }, }; }; - -export default generateJSONSchema; diff --git a/tools/lib/generateVersionNumber.ts b/tools/lib/generateVersionNumber.ts new file mode 100644 index 000000000..2e4acda38 --- /dev/null +++ b/tools/lib/generateVersionNumber.ts @@ -0,0 +1,12 @@ +export const generateVersionNumber = () => { + const date = new Date(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const minutes = String(date.getMinutes() + 60 * date.getHours()).padStart( + 4, + "0", + ); + + return `${year}.${month}.${day}.${minutes}`; +}; diff --git a/tools/lib/getSport.ts b/tools/lib/getSport.ts new file mode 100644 index 000000000..fefdb34a7 --- /dev/null +++ b/tools/lib/getSport.ts @@ -0,0 +1,11 @@ +const SPORTS = ["baseball", "basketball", "football", "hockey"] as const; + +export const getSport = () => { + if (SPORTS.includes(process.env.SPORT)) { + return process.env.SPORT; + } + if (process.env.SPORT === undefined) { + return "basketball"; + } + throw new Error(`Invalid SPORT: ${process.env.SPORT}`); +}; diff --git a/tools/lib/minifyIndexHtml.ts b/tools/lib/minifyIndexHtml.ts new file mode 100644 index 000000000..e73fd29f3 --- /dev/null +++ b/tools/lib/minifyIndexHtml.ts @@ -0,0 +1,15 @@ +import fs from "node:fs"; +import { minify } from "html-minifier-terser"; + +export const minifyIndexHtml = async () => { + const content = fs.readFileSync("build/index.html", "utf8"); + const minified = await minify(content, { + collapseBooleanAttributes: true, + collapseWhitespace: true, + minifyCSS: true, + minifyJS: true, + removeComments: true, + useShortDoctype: true, + }); + fs.writeFileSync("build/index.html", minified); +}; diff --git a/tools/lib/replace.ts b/tools/lib/replace.ts new file mode 100644 index 000000000..3cbd1c818 --- /dev/null +++ b/tools/lib/replace.ts @@ -0,0 +1,20 @@ +import fs from "node:fs"; + +export const replace = ({ + paths, + replaces, +}: { + paths: fs.PathOrFileDescriptor[]; + replaces: { + searchValue: string | RegExp; + replaceValue: string; + }[]; +}) => { + for (const path of paths) { + let contents = fs.readFileSync(path, "utf8"); + for (const { searchValue, replaceValue } of replaces) { + contents = contents.replaceAll(searchValue, replaceValue); + } + fs.writeFileSync(path, contents); + } +}; diff --git a/tools/lib/reset.ts b/tools/lib/reset.ts new file mode 100644 index 000000000..427aeddcc --- /dev/null +++ b/tools/lib/reset.ts @@ -0,0 +1,6 @@ +import fsp from "node:fs/promises"; + +export const reset = async () => { + await fsp.rm("build", { recursive: true, force: true }); + await fsp.mkdir("build/gen", { recursive: true }); +}; diff --git a/tools/lib/rollupConfig.ts b/tools/lib/rollupConfig.ts index a0793ad2c..6148eaaaa 100644 --- a/tools/lib/rollupConfig.ts +++ b/tools/lib/rollupConfig.ts @@ -9,7 +9,7 @@ import resolve from "@rollup/plugin-node-resolve"; import replace from "@rollup/plugin-replace"; import terser from "@rollup/plugin-terser"; import { visualizer } from "rollup-plugin-visualizer"; -import { getSport } from "./buildFuncs.ts"; +import { getSport } from "./getSport.ts"; const extensions = [".mjs", ".js", ".json", ".node", ".ts", ".tsx"]; diff --git a/tools/lib/setTimestamps.ts b/tools/lib/setTimestamps.ts new file mode 100644 index 000000000..6cd545616 --- /dev/null +++ b/tools/lib/setTimestamps.ts @@ -0,0 +1,281 @@ +import { bySport } from "./bySport.ts"; +import { replace } from "./replace.ts"; + +export const setTimestamps = (rev: string, watch: boolean = false) => { + if (watch) { + replace({ + paths: ["build/index.html"], + replaces: [ + { + searchValue: "-REV_GOES_HERE.js", + replaceValue: ".js", + }, + { + searchValue: '-" + bbgmVersion + "', + replaceValue: "", + }, + { + searchValue: /-CSS_HASH_(LIGHT|DARK)/g, + replaceValue: "", + }, + { + searchValue: "REV_GOES_HERE", + replaceValue: rev, + }, + ], + }); + } else { + replace({ + paths: [ + "build/index.html", + + // This is currently just for lastChangesVersion, so don't worry about it not working in watch mode + `build/gen/worker-${rev}.js`, + `build/gen/worker-legacy-${rev}.js`, + ], + replaces: [ + { + searchValue: "REV_GOES_HERE", + replaceValue: rev, + }, + ], + }); + } + + // Quantcast Choice. Consent Manager Tag v2.0 (for TCF 2.0) + const bannerAdsCode = ` + + + + + + + + + + +`; + + if (!watch) { + replace({ + paths: [`build/gen/ui-legacy-${rev}.js`], + replaces: [ + { + searchValue: "/gen/worker-", + replaceValue: "/gen/worker-legacy-", + }, + ], + }); + } + + replace({ + paths: ["build/index.html"], + replaces: [ + { + searchValue: "BANNER_ADS_CODE", + replaceValue: bannerAdsCode, + }, + { + searchValue: "GOOGLE_ANALYTICS_ID", + replaceValue: bySport({ + basketball: "UA-38759330-1", + football: "UA-38759330-2", + default: "UA-38759330-3", + }), + }, + { + searchValue: "BUGSNAG_API_KEY", + replaceValue: bySport({ + baseball: "37b1fd32d021f7716dc0e1d4a3e619bc", + basketball: "c10b95290070cb8888a7a79cc5408555", + football: "fed8957cbfca2d1c80997897b840e6cf", + hockey: "449e8ed576f7cbccf5c7649e936ab9ff", + }), + }, + ], + }); + + return rev; +}; diff --git a/tools/pre-test.ts b/tools/pre-test.ts index 6687e02b5..920311b8a 100644 --- a/tools/pre-test.ts +++ b/tools/pre-test.ts @@ -1,9 +1,8 @@ import fs from "node:fs"; if (!fs.existsSync("build/files/league-schema.json")) { - const generateJSONSchema = (await import("./lib/generateJSONSchema.ts")) - .default; - const jsonSchema = generateJSONSchema("test"); + const { generateJsonSchema } = await import("./lib/generateJsonSchema.ts"); + const jsonSchema = generateJsonSchema("test"); fs.mkdirSync("build/files", { recursive: true }); fs.writeFileSync( "build/files/league-schema.json", diff --git a/tools/watch/watchCSSWorker.ts b/tools/watch/watchCSSWorker.ts index c3541c253..ff7cbe18d 100644 --- a/tools/watch/watchCSSWorker.ts +++ b/tools/watch/watchCSSWorker.ts @@ -1,10 +1,10 @@ import { watch } from "chokidar"; import { parentPort } from "node:worker_threads"; -import { buildCSS } from "../lib/buildFuncs.ts"; +import { buildCss } from "../lib/buildCss.ts"; const filenames = ["build/gen/light.css", "build/gen/dark.css"]; -const myBuildCSS = async () => { +const mybuildCss = async () => { for (const filename of filenames) { parentPort!.postMessage({ type: "start", @@ -13,7 +13,7 @@ const myBuildCSS = async () => { } try { - await buildCSS(true); + await buildCss(true); for (const filename of filenames) { parentPort!.postMessage({ type: "end", @@ -31,7 +31,7 @@ const myBuildCSS = async () => { } }; -await myBuildCSS(); +await mybuildCss(); const watcher = watch("public/css", {}); -watcher.on("change", myBuildCSS); +watcher.on("change", mybuildCss); diff --git a/tools/watch/watchFiles.ts b/tools/watch/watchFiles.ts index 244c0ac36..c1bb300fd 100644 --- a/tools/watch/watchFiles.ts +++ b/tools/watch/watchFiles.ts @@ -1,5 +1,8 @@ import { watch } from "chokidar"; -import { copyFiles, genRev, reset, setTimestamps } from "../lib/buildFuncs.ts"; +import { copyFiles } from "../lib/copyFiles.ts"; +import { generateVersionNumber } from "../lib/generateVersionNumber.ts"; +import { reset } from "../lib/reset.ts"; +import { setTimestamps } from "../lib/setTimestamps.ts"; // Would be better to only copy individual files on update, but this is fast enough @@ -16,8 +19,8 @@ const watchFiles = async ( await copyFiles(true); - const rev = genRev(); - setTimestamps(rev, true); + const versionNumber = generateVersionNumber(); + setTimestamps(versionNumber, true); //minifyIndexHTML(); updateEnd(outFilename); diff --git a/tools/watch/watchJSONSchema.ts b/tools/watch/watchJSONSchema.ts index 0e2c184ea..aa27e99c0 100644 --- a/tools/watch/watchJSONSchema.ts +++ b/tools/watch/watchJSONSchema.ts @@ -1,11 +1,11 @@ import { watch } from "chokidar"; import fs from "node:fs"; -import { getSport } from "../lib/buildFuncs.ts"; +import { getSport } from "../lib/getSport.ts"; // https://ar.al/2021/02/22/cache-busting-in-node.js-dynamic-esm-imports/ const importFresh = async (modulePath: string) => { const cacheBustingModulePath = `${modulePath}?update=${Date.now()}`; - return (await import(cacheBustingModulePath)).default; + return await import(cacheBustingModulePath); }; const watchJSONSchema = async ( @@ -23,12 +23,12 @@ const watchJSONSchema = async ( try { updateStart(outFilename); - // Dynamically reload generateJSONSchema, cause that's what we're watching! - const generateJSONSchema = await importFresh( - "../lib/generateJSONSchema.ts", + // Dynamically reload generateJsonSchema, cause that's what we're watching! + const { generateJsonSchema } = await importFresh( + "../lib/generateJsonSchema.ts", ); - const jsonSchema = generateJSONSchema(sport); + const jsonSchema = generateJsonSchema(sport); const output = JSON.stringify(jsonSchema, null, 2); fs.writeFileSync(outFilename, output); @@ -40,7 +40,7 @@ const watchJSONSchema = async ( await buildJSONSchema(); - const watcher = watch("tools/lib/generateJSONSchema.ts", {}); + const watcher = watch("tools/lib/generateJsonSchema.ts", {}); watcher.on("change", buildJSONSchema); };