Skip to content

Commit

Permalink
Basics for web
Browse files Browse the repository at this point in the history
  • Loading branch information
zakstucke committed Nov 28, 2023
1 parent ba5cfd3 commit 9cea4ab
Show file tree
Hide file tree
Showing 9 changed files with 7,806 additions and 2,495 deletions.
1 change: 1 addition & 0 deletions js/bitbazaar/color/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createSteppedScale } from "./steppedScale";
71 changes: 71 additions & 0 deletions js/bitbazaar/color/steppedScale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ChromaStatic } from "chroma-js";

interface SteppedScaleProps {
/** Chroma instance to use, allows using a modified version to reduce bundle size downstream if needed. */
chroma: ChromaStatic;
/** The color to create a scale around. */
color: string;
/** The number of steps to create. Including the base color entered. */
numberOfSteps: number;
}

/** Creates a scale around an input color with the requested number of steps.
* E.g. 5 steps requested, it will go: darker1, darker2, input, lighter1, lighter2.
*/
export const createSteppedScale = ({
chroma,
color,
numberOfSteps,
}: SteppedScaleProps): string[] => {
const baseHex = chroma(color).hex().toLowerCase();
const whiteHex = chroma("white").hex().toLowerCase();
const blackHex = chroma("black").hex().toLowerCase();

// If its white or black, just return the same for all steps:
if (baseHex === whiteHex || baseHex === blackHex) {
return Array(numberOfSteps).fill(baseHex);
}

const baseNum = Math.ceil(numberOfSteps / 2);

// Try up to 5 times to produce values that don't end in white or black (i.e. the step size too large)
const numAttempts = 5;
for (let attempt = 1; attempt <= numAttempts; attempt++) {
const isFinalAttempt = attempt === numAttempts;

const steps: string[] = [];
// Reduce the step size each attempt, to try and get a scale that doesn't hit white or black:
const stepSize = 0.5 * (1 / attempt);
let failed = false;
for (let i = 1; i <= numberOfSteps; i++) {
let derivCol: string;
if (i < baseNum) {
derivCol = chroma(color)
.darken((baseNum - i) * stepSize)
.hex();
} else if (i === baseNum) {
derivCol = baseHex;
} else {
derivCol = chroma(color)
.brighten((i - baseNum) * stepSize)
.hex();
}

// If we've hit white or black (and isn't final attempt), step size still too large, try again with smaller:
if (!isFinalAttempt && (derivCol === whiteHex || derivCol === blackHex)) {
failed = true;
break;
}

steps.push(derivCol);
}

if (!failed) {
return steps;
}
}

throw new Error(
`Failed to create scale for color: ${color} with ${numberOfSteps} steps within the attempt limit of ${numAttempts}.`,
);
};
289 changes: 289 additions & 0 deletions js/bitbazaar/vite/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import preact from "@preact/preset-vite";
import { minify } from "html-minifier-terser";
import { resolve } from "path";
import checker from "vite-plugin-checker";
import Inspect from "vite-plugin-inspect";
import { VitePWA } from "vite-plugin-pwa";
import { defineConfig, UserConfig } from "vitest/config";

import fs from "fs/promises";

import { genPath } from "./genPath";
import { genBackendProxies, ProxyConf } from "./genProxy";

const baseNonFrontendGlobs: string[] = [
"**/.git/**",
"**/backend/**",
"**/venv/**",
"**/.venv/**",
"**/node_modules/**",
"**/dist/**",
"**/cypress/**",
"**/test_media/**",
"**/static/**",
"**/coverage/**",
"**/process_data/**",
"**/htmlcov/**",
"**/.{idea,git,cache,output,temp,mypy_cache,pytype}/**",
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*",
];

export interface TopViteConfig {
siteName: string;
siteDescription: string;

/** The absolute url online that `staticPath` files can be retrieved from
* E.g. https://example.com/static
*/
staticUrl: string;
/** The os path to the files that will deployed to `staticUrl`.
* E.g. /.../static
*/
staticPath: string;

/** The absolute url online that `sameDomStaticPath` files can be retrieved from
* Note if the static files are hosted on the same domain (i.e. not linking to a cdn, these can be the same as main static version)
* E.g. https://example.com/~sds
*/
sameDomStaticUrl: string;
/** The os path to the files that will deployed to `sameDomStaticUrl`.
* Note if the static files are hosted on the same domain (i.e. not linking to a cdn, these can be the same as main static version)
* E.g. /.../~sds
*/
sameDomStaticPath: string;

favicon192PngPath: string;
favicon512PngPath: string;

/** Proxy rules to apply to the dev server (i.e. when it should forward requests to a backend) */
proxy: ProxyConf;

/** Extra globs to exclude from traversal, can help with performance: */
extraNonFrontendGlobs?: string[];

/** Include the inspect plugin, which allows you to see how vite is transforming code: */
inspect?: boolean;
}

/** Vite config creator
* CSS:
* - css/scss is handled automatically, postcss.config.cjs is being detected automatically
* - foo.module.s?css identifies local, everything else is treated as global
* - (for potential future compatibility still globals as write as foo.global.s?css)
*
* HTML ENTRY:
* - Vite looks for an index.html file at the root, there's currently no way to configure this.
* - If you need to preprocess in any way, e.g. django or etch. You'll have to have a source you preprocess first.
* - Vite will process it further
* - The final minified index.html will be added to the assets folder, where it should be the root of a static site, or server manually from a backend server.
*/
export const createConfig = (mode: string, conf: TopViteConfig): UserConfig => {
const isProd = mode === "production";
const isTest = mode === "test";
const isDev = mode === "development";

if (!isProd && !isTest && !isDev) {
throw new Error(`Unexpected vite mode: ${mode}`);
}

// eslint-disable-next-line no-console
console.log(`Vite mode: ${mode}, prod=${isProd}, test=${isTest}, dev=${isDev}`);

const assetsPath = genPath(conf.staticPath, {
extra: ["dist"],
});
const assetsUrl = genPath(conf.staticUrl, {
extra: ["dist"],
});

const nonFrontendGlobs = [...baseNonFrontendGlobs, ...(conf.extraNonFrontendGlobs || [])];
const plugins: UserConfig["plugins"] = [
preact({
devToolsEnabled: !isProd,
prefreshEnabled: !isProd,
}),
// The service worker / pwa stuff:
VitePWA({
registerType: "autoUpdate",
workbox: {
// Precache everything found in the vite dist folder:
globDirectory: genPath(assetsPath),
// Excluding html, we only have one root index.html and want that to always be fresh:
globPatterns: ["**/*.{js,css,ico,png,svg}"],

// Don't fallback on document based (e.g. `/some-page`) requests
// Even though this says `null` by default, I had to set this specifically to `null` to make it work
navigateFallback: null,

// Tell the worker it should be finding all the files from the static domain, rather than the backend:
modifyURLPrefix: {
"": genPath(assetsUrl),
},
},

// Needs to come from the backend/same domain otherwise CORS won't work (classic worker problems)
srcDir: genPath(assetsPath),
outDir: genPath(conf.sameDomStaticPath, {
extra: ["sworker"],
}),

// Note the mainfest.webmanifest is incorrectly placed in static/dist, but the links go the backend url.
// Bug in the lib, haven't found a proper way to solve.
// A secondary plugin below moves the file at the end.
base: genPath(conf.sameDomStaticUrl, {
extra: ["sworker"],
}),
buildBase: genPath(conf.sameDomStaticUrl, {
extra: ["sworker"],
}),

// Puts the sw importer script directly in the index.html head, rather than a separate file:
injectRegister: "inline",

// Allowing the service worker to control the full stie rather than the dir it comes from:
// this requires the Service-Worker-Allowed: "/" header to be passed from the worker serve directory on backend (which it is)
scope: "/",

manifest: {
name: conf.siteName,
short_name: conf.siteName,
description: conf.siteDescription,
theme_color: "#031033",
background_color: "#031033",
display: "standalone",
start_url: "/",
icons: [
{
src: conf.favicon192PngPath,
sizes: "192x192",
type: "image/png",
purpose: "any maskable",
},
{
src: conf.favicon512PngPath,
sizes: "512x512",
type: "image/png",
purpose: "any maskable",
},
],
},
}),
{
name: "move-webmanifest", // the name of your custom plugin. Could be anything.
apply: "build",
enforce: "post",
closeBundle: async () => {
const oldLoc = genPath(assetsPath, {
extra: ["manifest.webmanifest"],
});
const newLoc = genPath(conf.sameDomStaticPath, {
extra: ["sworker", "manifest.webmanifest"],
});
// eslint-disable-next-line no-console
console.log(`Moving webmanifest from ${oldLoc} to ${newLoc}`);
await fs.rename(oldLoc, newLoc);

// Also copy and minify the index.html file to the same domain static dir, which is where nginx/backend expects it to be:
const indexLoc = genPath(assetsPath, {
extra: ["index.html"],
});
// eslint-disable-next-line no-console
console.log(`Reading index.html from ${oldLoc} and minifying...`);
const indexSrc = await fs.readFile(indexLoc, "utf-8");
const indexMinified = await minify(indexSrc, {
collapseBooleanAttributes: true,
collapseInlineTagWhitespace: true,
collapseWhitespace: true,
useShortDoctype: true,
removeComments: true,
minifyCSS: true,
minifyJS: true,
});
// Naming base rather than index to work more nicely with nginx:
const newIndexLoc = genPath(conf.sameDomStaticPath, {
extra: ["base.html"],
});
// eslint-disable-next-line no-console
console.log(
`Minified index.html from ${indexSrc.length} to ${indexMinified.length} bytes, writing to ${newIndexLoc}.`,
);
await fs.writeFile(newIndexLoc, indexMinified);
},
},
];

// Can be monitor how code is transformed / plugins used at http://localhost:3000/__inspect
if (isDev && conf.inspect) {
plugins.push(Inspect());
}

// Don't add compile checks when testing to keep speedy:
if (!isTest) {
plugins.push(
checker({
typescript: true,
enableBuild: true, // Want it to validate type checking on build too
// Show errors in terminal & UI:
terminal: true,
overlay: true,
}),
);
}

const config: UserConfig = {
clearScreen: false, // Don't clear screen when running vite commands
plugins,
resolve: {
// Providing absolute paths:
alias: {
helpers: resolve("./frontend/src/helpers"),
apps: resolve("./frontend/src/apps"),
scss_global: resolve("./frontend/src/scss_global"),
fbs: resolve("./front_back_shared"),
},
// Fixes esm problem with rrd:
// react-router-dom specifies "module" field in package.json for ESM entry
// if it's not mapped, it uses the "main" field which is CommonJS that redirects to CJS preact
mainFields: ["module"],
},
server: {
// open: true, // Open the web page on start
host: "localhost",
port: 3000,
watch: {
// Don't check non frontend files:
ignored: nonFrontendGlobs,
},
// Proxy all backend requests to django/fastapi:
proxy: genBackendProxies(conf.proxy),
},
// When being served from django in production, js internal assets urls need to use url to the js assets inside the static url,
// rather than in dev where they're just served from the root:
base: genPath(isProd ? assetsUrl : "/"),
// The production build config:
build: {
outDir: genPath(assetsPath),
},
define: {
// Vite doesn't have process, so define the specifically needed ones directly:
"process.env.NODE_ENV": JSON.stringify(mode),
},

// Only enabled during dev by default,
// this tells esbuild to look and see what node_modules are being used from each js file,
// then pre-optimises them.
optimizeDeps: {
entries: ["./frontend/**/*.{js,jsx,ts,tsx}", "./index.html"],
},

// (Attempted!) size and perf optimisations:
json: {
stringify: true,
},
esbuild: {
legalComments: "none",
exclude: nonFrontendGlobs,
},
};
return defineConfig(config);
};
Loading

0 comments on commit 9cea4ab

Please sign in to comment.