Restructure: move Vite project into app/, deploy build output to repo root
The webserver serves the repo checkout directly as static files, so index.html at the repo root must be the production build (pointing at hashed /assets/*.js) rather than the dev template (/src/main.tsx), which was being served with an unrecognized application/octet-stream MIME type and blocked by the browser. - Move src/, public/, scripts/, server/, package.json, vite.config.ts, tsconfig.json, .env(.example), and the dev index.html into app/. - Add app/scripts/deploy.mjs: copies app/dist/* into the repo root after every `vite build`, cleaning up stale hashed filenames. - Wire it into `npm run build` (app/package.json). - Update run.sh to operate from app/, and .gitignore for the new layout. - Commit the current build output (index.html, assets/, manifest, sw.js, workbox-*.js, favicon.svg, icons/, LICENSE) to the repo root. - Update README with the new project layout and deployment model.
This commit is contained in:
1 parent
5245c0a856
commit
c7b653ff6a
54 files changed
+224
-34
No files matched your search
@@ -0,0 +1,48 @@
|
||||
// Copies the production build (app/dist) into the repository root, so the
|
||||
// webserver can serve the repo checkout directly as static files — no
|
||||
// server-side build step, no special MIME-type config, no reverse proxy
|
||||
// required. Run automatically as part of `npm run build`.
|
||||
//
|
||||
// node scripts/deploy.mjs
|
||||
//
|
||||
import { readdir, rm, cp, unlink, copyFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DIST = resolve(__dirname, "../dist");
|
||||
const REPO_ROOT = resolve(__dirname, "../../");
|
||||
|
||||
const entries = await readdir(DIST, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const src = join(DIST, entry.name);
|
||||
const dest = join(REPO_ROOT, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Directories (assets/, icons/) are pure build output — wipe and replace
|
||||
// so stale, superseded hashed filenames don't pile up.
|
||||
await rm(dest, { recursive: true, force: true });
|
||||
await cp(src, dest, { recursive: true });
|
||||
console.log(` dir ${entry.name}/`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Hashed, content-addressed files (e.g. workbox-<hash>.js) change name
|
||||
// between builds — remove any previous versions before copying the new one.
|
||||
const hashedPrefixMatch = entry.name.match(/^(workbox-)[0-9a-f]+(\.js)$/);
|
||||
if (hashedPrefixMatch) {
|
||||
const [, prefix, ext] = hashedPrefixMatch;
|
||||
const siblings = await readdir(REPO_ROOT);
|
||||
for (const sibling of siblings) {
|
||||
if (sibling.startsWith(prefix) && sibling.endsWith(ext) && sibling !== entry.name) {
|
||||
await unlink(join(REPO_ROOT, sibling));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await copyFile(src, dest);
|
||||
console.log(` file ${entry.name}`);
|
||||
}
|
||||
|
||||
console.log(`\nDeployed build output from ${DIST} -> ${REPO_ROOT}`);
|
||||
@@ -0,0 +1,213 @@
|
||||
// Generates the PWA icon set with no external dependencies.
|
||||
// A tiny, correct PNG encoder (RGBA, filter 0) built on Node's zlib.
|
||||
//
|
||||
// node scripts/gen-icons.mjs
|
||||
//
|
||||
import { deflateSync } from "node:zlib";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT = resolve(__dirname, "../public/icons");
|
||||
|
||||
// ---- PNG encoding -----------------------------------------------------------
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
t[n] = c >>> 0;
|
||||
}
|
||||
return t;
|
||||
})();
|
||||
|
||||
function crc32(buf) {
|
||||
let c = 0xffffffff;
|
||||
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
||||
return (c ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function chunk(type, data) {
|
||||
const len = Buffer.alloc(4);
|
||||
len.writeUInt32BE(data.length, 0);
|
||||
const typeBuf = Buffer.from(type, "ascii");
|
||||
const crcBuf = Buffer.alloc(4);
|
||||
crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
|
||||
return Buffer.concat([len, typeBuf, data, crcBuf]);
|
||||
}
|
||||
|
||||
function encodePng(width, height, rgba) {
|
||||
const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(width, 0);
|
||||
ihdr.writeUInt32BE(height, 4);
|
||||
ihdr[8] = 8; // bit depth
|
||||
ihdr[9] = 6; // color type: RGBA
|
||||
const stride = width * 4;
|
||||
const raw = Buffer.alloc((stride + 1) * height);
|
||||
for (let y = 0; y < height; y++) {
|
||||
raw[y * (stride + 1)] = 0; // filter: none
|
||||
rgba.copy(raw, y * (stride + 1) + 1, y * stride, y * stride + stride);
|
||||
}
|
||||
const idat = deflateSync(raw, { level: 9 });
|
||||
return Buffer.concat([sig, chunk("IHDR", ihdr), chunk("IDAT", idat), chunk("IEND", Buffer.alloc(0))]);
|
||||
}
|
||||
|
||||
// ---- Drawing ----------------------------------------------------------------
|
||||
|
||||
function makeCanvas(size) {
|
||||
return { size, data: Buffer.alloc(size * size * 4, 0) };
|
||||
}
|
||||
|
||||
function blend(canvas, x, y, [r, g, b], a) {
|
||||
if (x < 0 || y < 0 || x >= canvas.size || y >= canvas.size || a <= 0) return;
|
||||
const i = (y * canvas.size + x) * 4;
|
||||
const d = canvas.data;
|
||||
const sa = Math.min(1, a);
|
||||
const da = d[i + 3] / 255;
|
||||
const outA = sa + da * (1 - sa);
|
||||
if (outA === 0) return;
|
||||
d[i] = Math.round((r * sa + d[i] * da * (1 - sa)) / outA);
|
||||
d[i + 1] = Math.round((g * sa + d[i + 1] * da * (1 - sa)) / outA);
|
||||
d[i + 2] = Math.round((b * sa + d[i + 2] * da * (1 - sa)) / outA);
|
||||
d[i + 3] = Math.round(outA * 255);
|
||||
}
|
||||
|
||||
// Signed distance to a rounded rectangle, used for antialiased edges.
|
||||
function roundedRectSdf(px, py, cx, cy, hw, hh, r) {
|
||||
const qx = Math.abs(px - cx) - (hw - r);
|
||||
const qy = Math.abs(py - cy) - (hh - r);
|
||||
const ax = Math.max(qx, 0);
|
||||
const ay = Math.max(qy, 0);
|
||||
return Math.sqrt(ax * ax + ay * ay) + Math.min(Math.max(qx, qy), 0) - r;
|
||||
}
|
||||
|
||||
function fillRoundedRect(canvas, cx, cy, hw, hh, r, color, alpha = 1) {
|
||||
const { size } = canvas;
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
const d = roundedRectSdf(x + 0.5, y + 0.5, cx, cy, hw, hh, r);
|
||||
const cov = Math.min(1, Math.max(0, 0.5 - d));
|
||||
if (cov > 0) blend(canvas, x, y, color, cov * alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function gradientAt(y, size, top, bottom) {
|
||||
const t = Math.max(0, Math.min(1, y / (size - 1)));
|
||||
return [
|
||||
Math.round(top[0] + (bottom[0] - top[0]) * t),
|
||||
Math.round(top[1] + (bottom[1] - top[1]) * t),
|
||||
Math.round(top[2] + (bottom[2] - top[2]) * t),
|
||||
];
|
||||
}
|
||||
|
||||
function fillRoundedRectGradient(canvas, cx, cy, hw, hh, r, top, bottom) {
|
||||
const { size } = canvas;
|
||||
for (let y = 0; y < size; y++) {
|
||||
const color = gradientAt(y + 0.5, size, top, bottom);
|
||||
for (let x = 0; x < size; x++) {
|
||||
const d = roundedRectSdf(x + 0.5, y + 0.5, cx, cy, hw, hh, r);
|
||||
const cov = Math.min(1, Math.max(0, 0.5 - d));
|
||||
if (cov > 0) blend(canvas, x, y, color, cov);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillCircle(canvas, cx, cy, r, color, alpha = 1) {
|
||||
const { size } = canvas;
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
const d = Math.hypot(x + 0.5 - cx, y + 0.5 - cy) - r;
|
||||
const cov = Math.min(1, Math.max(0, 0.5 - d));
|
||||
if (cov > 0) blend(canvas, x, y, color, cov * alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// derez.ai palette
|
||||
const TOP = [0x00, 0x35, 0x45];
|
||||
const BOTTOM = [0x00, 0x20, 0x2e];
|
||||
const CYAN = [0x00, 0xf5, 0xff];
|
||||
|
||||
// Draws the derez.ai mark: a teal tile with the cyan terminal glyph.
|
||||
function drawIcon(size, { maskable }) {
|
||||
const canvas = makeCanvas(size);
|
||||
const s = size;
|
||||
const pad = maskable ? 0 : s * 0.05;
|
||||
const radius = maskable ? 0 : s * 0.24;
|
||||
|
||||
fillRoundedRectGradient(canvas, s / 2, s / 2, s / 2 - pad, s / 2 - pad, radius, TOP, BOTTOM);
|
||||
|
||||
// The glyph is designed in a 34x34 box; scale it into the tile.
|
||||
const k = (s * (maskable ? 0.7 : 0.84)) / 34;
|
||||
const ox = (s - 34 * k) / 2;
|
||||
const oy = (s - 34 * k) / 2;
|
||||
const X = (v) => ox + v * k;
|
||||
const Y = (v) => oy + v * k;
|
||||
const stroke = 1.8 * k;
|
||||
|
||||
// Monitor outline: cyan rounded rect, then erase the interior with the tile gradient.
|
||||
const mcx = X(17);
|
||||
const mcy = Y(18.5);
|
||||
const mhw = 11 * k;
|
||||
const mhh = 7.5 * k;
|
||||
const mr = 2.5 * k;
|
||||
fillRoundedRect(canvas, mcx, mcy, mhw, mhh, mr, CYAN, 1);
|
||||
fillRoundedRectGradient(
|
||||
canvas,
|
||||
mcx,
|
||||
mcy,
|
||||
mhw - stroke,
|
||||
mhh - stroke,
|
||||
Math.max(0, mr - stroke),
|
||||
TOP,
|
||||
BOTTOM,
|
||||
);
|
||||
|
||||
// Antenna stalk + dot.
|
||||
fillRoundedRect(canvas, X(17), Y(9), stroke / 2, 2 * k, stroke / 2, CYAN, 1);
|
||||
fillCircle(canvas, X(17), Y(5.5), 1.5 * k, CYAN);
|
||||
|
||||
// Indicator dots.
|
||||
fillCircle(canvas, X(10), Y(15.5), 1 * k, CYAN);
|
||||
fillCircle(canvas, X(14), Y(15.5), 1 * k, CYAN);
|
||||
|
||||
// Bottom bar.
|
||||
fillRoundedRect(canvas, X(17), Y(20), 7 * k, 0.75 * k, 0.75 * k, CYAN, 1);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function write(name, canvas) {
|
||||
writeFileSync(resolve(OUT, name), encodePng(canvas.size, canvas.size, canvas.data));
|
||||
console.log("wrote", name, `${canvas.size}x${canvas.size}`);
|
||||
}
|
||||
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
write("icon-192.png", drawIcon(192, { maskable: false }));
|
||||
write("icon-512.png", drawIcon(512, { maskable: false }));
|
||||
write("maskable-512.png", drawIcon(512, { maskable: true }));
|
||||
write("apple-touch-icon.png", drawIcon(180, { maskable: false }));
|
||||
|
||||
// Matching SVG favicon (the derez.ai mark, crisp at any size).
|
||||
const favicon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 34">
|
||||
<defs>
|
||||
<linearGradient id="b" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#003545"/>
|
||||
<stop offset="100%" stop-color="#00202e"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="34" height="34" rx="10" fill="url(#b)" stroke="#005570" stroke-width="1.2"/>
|
||||
<rect x="6" y="11" width="22" height="15" rx="2.5" fill="none" stroke="#00f5ff" stroke-width="1.8"/>
|
||||
<path d="M17 11V7" stroke="#00f5ff" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<circle cx="17" cy="5.5" r="1.5" fill="#00f5ff"/>
|
||||
<circle cx="10" cy="15.5" r="1" fill="#00f5ff"/>
|
||||
<circle cx="14" cy="15.5" r="1" fill="#00f5ff"/>
|
||||
<path d="M10 20h14" stroke="#00f5ff" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
`;
|
||||
writeFileSync(resolve(__dirname, "../public/favicon.svg"), favicon);
|
||||
console.log("wrote favicon.svg");
|
||||
Reference in new issue
Block a user